diff --git a/cmd/root.go b/cmd/root.go index f895110c..7efccce7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -38,7 +38,6 @@ var ( noExitFlag bool maxSteps int streamFlag bool // Enable streaming output - compactMode bool // Enable compact output mode autoCompactFlag bool // Enable auto-compaction near context limit // Session management @@ -280,8 +279,6 @@ func init() { IntVar(&maxSteps, "max-steps", 0, "maximum number of agent steps (0 for unlimited)") rootCmd.PersistentFlags(). BoolVar(&streamFlag, "stream", true, "enable streaming output for faster response display") - rootCmd.PersistentFlags(). - BoolVar(&compactMode, "compact", false, "enable compact output mode without fancy styling") rootCmd.PersistentFlags(). BoolVar(&autoCompactFlag, "auto-compact", false, "auto-compact conversation when near context limit") rootCmd.PersistentFlags(). @@ -325,7 +322,6 @@ func init() { _ = viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")) _ = viper.BindPFlag("max-steps", rootCmd.PersistentFlags().Lookup("max-steps")) _ = viper.BindPFlag("stream", rootCmd.PersistentFlags().Lookup("stream")) - _ = viper.BindPFlag("compact", rootCmd.PersistentFlags().Lookup("compact")) _ = viper.BindPFlag("auto-compact", rootCmd.PersistentFlags().Lookup("auto-compact")) _ = viper.BindPFlag("provider-url", rootCmd.PersistentFlags().Lookup("provider-url")) @@ -728,7 +724,7 @@ func runNormalMode(ctx context.Context) error { var spinnerFunc kit.SpinnerFunc if !quietFlag { spinnerFunc = func(fn func() error) error { - tempCli, tempErr := ui.NewCLI(viper.GetBool("debug"), viper.GetBool("compact")) + tempCli, tempErr := ui.NewCLI(viper.GetBool("debug")) if tempErr == nil { return tempCli.ShowSpinner(fn) } @@ -1804,7 +1800,6 @@ func runInteractiveModeBubbleTea(_ context.Context, appInstance *app.App, modelN cwd, _ := os.Getwd() appModel := ui.NewAppModel(appInstance, ui.AppModelOptions{ - CompactMode: viper.GetBool("compact"), ModelName: modelName, ProviderName: providerName, LoadingMessage: loadingMessage, diff --git a/cmd/setup.go b/cmd/setup.go index f8a868ce..94f25d30 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -41,7 +41,6 @@ func BuildAppOptions(mcpConfig *config.Config, modelName string, serverNames, to StreamingEnabled: viper.GetBool("stream"), Quiet: quietFlag, Debug: viper.GetBool("debug"), - CompactMode: viper.GetBool("compact"), } } @@ -131,7 +130,6 @@ func SetupCLIForNonInteractive(k *kit.Kit) (*ui.CLI, error) { Agent: agentAdapter, ModelString: viper.GetString("model"), Debug: viper.GetBool("debug"), - Compact: viper.GetBool("compact"), Quiet: quietFlag, ShowDebug: false, ProviderAPIKey: viper.GetString("provider-api-key"), diff --git a/internal/app/options.go b/internal/app/options.go index 9839c0d8..7c1b9a76 100644 --- a/internal/app/options.go +++ b/internal/app/options.go @@ -67,10 +67,6 @@ type Options struct { // Debug enables verbose debug logging. Debug bool - // CompactMode selects the compact renderer instead of the block renderer for - // message formatting. - CompactMode bool - // UsageTracker is an optional callback for recording token usage after each // agent step. When non-nil, the app layer calls UpdateUsage (or // EstimateAndUpdateUsage as a fallback) using the usage data returned by the diff --git a/internal/config/config.go b/internal/config/config.go index 42839ff0..194e4c6d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -191,7 +191,6 @@ type Config struct { Model string `json:"model,omitempty" yaml:"model,omitempty"` MaxSteps int `json:"max-steps,omitempty" yaml:"max-steps,omitempty"` Debug bool `json:"debug,omitempty" yaml:"debug,omitempty"` - Compact bool `json:"compact,omitempty" yaml:"compact,omitempty"` SystemPrompt string `json:"system-prompt,omitempty" yaml:"system-prompt,omitempty"` ProviderAPIKey string `json:"provider-api-key,omitempty" yaml:"provider-api-key,omitempty"` ProviderURL string `json:"provider-url,omitempty" yaml:"provider-url,omitempty"` diff --git a/internal/ui/children_test.go b/internal/ui/children_test.go index 46b36133..a785714d 100644 --- a/internal/ui/children_test.go +++ b/internal/ui/children_test.go @@ -275,10 +275,9 @@ func TestInputComponent_UnknownSlashCommand_ForwardsAsSubmit(t *testing.T) { // Helpers // -------------------------------------------------------------------------- -// newTestStream creates a StreamComponent with a fixed width and model name, -// in non-compact mode. +// newTestStream creates a StreamComponent with a fixed width and model name. func newTestStream() *StreamComponent { - return NewStreamComponent(false, 80, "test-model") + return NewStreamComponent(80, "test-model") } // sendStreamMsg calls component.Update and returns the updated component. diff --git a/internal/ui/cli.go b/internal/ui/cli.go index 11f06263..56748276 100644 --- a/internal/ui/cli.go +++ b/internal/ui/cli.go @@ -11,33 +11,26 @@ import ( ) // CLI manages the command-line interface for KIT, providing message rendering, -// user input handling, and display management. It supports both standard and compact -// display modes, handles streaming responses, tracks token usage, and manages the -// overall conversation flow between the user and AI assistants. +// user input handling, and display management. It handles streaming responses, +// tracks token usage, and manages the overall conversation flow between the +// user and AI assistants. type CLI struct { renderer Renderer usageTracker *UsageTracker width int - compactMode bool debug bool modelName string } -// NewCLI creates and initializes a new CLI instance with the specified display modes. -// The debug parameter enables debug message rendering, while compact enables a more -// condensed display format. Returns an initialized CLI ready for interaction or an +// NewCLI creates and initializes a new CLI instance. The debug parameter enables +// debug message rendering. Returns an initialized CLI ready for interaction or an // error if initialization fails. -func NewCLI(debug bool, compact bool) (*CLI, error) { +func NewCLI(debug bool) (*CLI, error) { cli := &CLI{ - compactMode: compact, - debug: debug, + debug: debug, } cli.updateSize() - if compact { - cli.renderer = NewCompactRenderer(cli.width, debug) - } else { - cli.renderer = newMessageRenderer(cli.width, debug) - } + cli.renderer = newMessageRenderer(cli.width, debug) return cli, nil } diff --git a/internal/ui/compact_renderer.go b/internal/ui/compact_renderer.go deleted file mode 100644 index 9f5287d6..00000000 --- a/internal/ui/compact_renderer.go +++ /dev/null @@ -1,480 +0,0 @@ -package ui - -import ( - "fmt" - "strings" - "time" - - "charm.land/lipgloss/v2" -) - -// CompactRenderer handles rendering messages in a space-efficient compact format, -// optimized for terminals with limited vertical space. It displays messages with -// minimal decorations while maintaining readability and essential information. -type CompactRenderer struct { - width int - debug bool - - // getToolRenderer returns extension-provided rendering overrides for a - // specific tool. May be nil if no extensions are loaded. Used in - // RenderToolMessage to check for custom header/body formatting before - // falling back to builtin renderers. - getToolRenderer func(toolName string) *ToolRendererData -} - -// NewCompactRenderer creates and initializes a new CompactRenderer with the specified -// terminal width and debug mode setting. The width parameter determines line wrapping, -// while debug enables additional diagnostic output in rendered messages. -func NewCompactRenderer(width int, debug bool) *CompactRenderer { - return &CompactRenderer{ - width: width, - debug: debug, - } -} - -// SetWidth updates the terminal width for the renderer, affecting how content -// is wrapped and formatted in subsequent render operations. -func (r *CompactRenderer) SetWidth(width int) { - r.width = width -} - -// RenderUserMessage renders a user's input message in compact format with a -// distinctive symbol (>) and label. The content is formatted to preserve structure -// while minimizing vertical space usage. Returns a UIMessage with formatted content -// and metadata. -func (r *CompactRenderer) RenderUserMessage(content string, timestamp time.Time) UIMessage { - theme := GetTheme() - symbol := lipgloss.NewStyle().Foreground(theme.Info).Render(">") - label := lipgloss.NewStyle().Foreground(theme.Info).Bold(true).Render("User") - - // Only run markdown rendering when the message contains code spans or - // fenced code blocks. Plain text is rendered directly so that newlines - // are preserved without the extra paragraph spacing glamour adds. - var compactContent string - if strings.Contains(content, "`") { - mdContent := strings.ReplaceAll(content, "\n", "\n\n") - compactContent = r.formatUserAssistantContent(mdContent) - compactContent = removeBlankLines(compactContent) - } else { - compactContent = content - } - - // Handle multi-line content - lines := strings.Split(compactContent, "\n") - var formattedLines []string - - for i, line := range lines { - if i == 0 { - // First line includes symbol and label - formattedLines = append(formattedLines, fmt.Sprintf("%s %s %s", symbol, label, line)) - } else { - // Subsequent lines without indentation for compact mode - formattedLines = append(formattedLines, line) - } - } - - return UIMessage{ - Type: UserMessage, - Content: strings.Join(formattedLines, "\n"), - Height: len(formattedLines), - Timestamp: timestamp, - } -} - -// RenderAssistantMessage renders an AI assistant's response in compact format with -// a distinctive symbol (<) and the model name as label. Empty content is ignored -// and returns an empty message. Returns a UIMessage with formatted content and metadata. -func (r *CompactRenderer) RenderAssistantMessage(content string, timestamp time.Time, modelName string) UIMessage { - // Ignore empty responses - don't render anything - compactContent := r.formatUserAssistantContent(content) - if compactContent == "" { - return UIMessage{ - Type: AssistantMessage, - Content: "", - Height: 0, - Timestamp: timestamp, - } - } - - theme := GetTheme() - symbol := lipgloss.NewStyle().Foreground(theme.Primary).Render("<") - - // Use the full model name, fallback to "Assistant" if empty - if modelName == "" { - modelName = "Assistant" - } - label := lipgloss.NewStyle().Foreground(theme.Primary).Bold(true).Render(modelName) - - // Handle multi-line content - lines := strings.Split(compactContent, "\n") - var formattedLines []string - - for i, line := range lines { - if i == 0 { - // First line includes symbol and label - formattedLines = append(formattedLines, fmt.Sprintf("%s %s %s", symbol, label, line)) - } else { - // Subsequent lines without indentation for compact mode - formattedLines = append(formattedLines, line) - } - } - - return UIMessage{ - Type: AssistantMessage, - Content: strings.Join(formattedLines, "\n"), - Height: len(formattedLines), - Timestamp: timestamp, - } -} - -// RenderReasoningBlock renders a reasoning/thinking block in compact format -// with the same muted italic styling as live streaming. This is used when -// resuming sessions to display saved reasoning content. -func (r *CompactRenderer) RenderReasoningBlock(content string, timestamp time.Time) UIMessage { - if strings.TrimSpace(content) == "" { - return UIMessage{ - Type: AssistantMessage, - Content: "", - Height: 0, - Timestamp: timestamp, - } - } - - theme := GetTheme() - // Match live streaming styling: muted italic text - // Compact mode uses direct lipgloss styling (no typography) - lines := strings.Split(strings.TrimRight(content, "\n"), "\n") - contentStr := strings.TrimLeft(strings.Join(lines, "\n"), " \t\n") - mutedStyle := lipgloss.NewStyle().Foreground(theme.Muted).Italic(true) - rendered := mutedStyle.Render(contentStr) - - return UIMessage{ - Type: AssistantMessage, - Content: rendered, - Height: lipgloss.Height(rendered), - Timestamp: timestamp, - } -} - -// RenderToolMessage renders a unified tool block in compact format, combining -// the tool invocation header (icon + display name + params) with the execution -// result body. Status is indicated by icon: checkmark for success, cross for error. -func (r *CompactRenderer) RenderToolMessage(toolName, toolArgs, toolResult string, isError bool) UIMessage { - theme := GetTheme() - - // Resolve extension renderer once for all overrides. - var extRd *ToolRendererData - if r.getToolRenderer != nil { - extRd = r.getToolRenderer(toolName) - } - - // Status icon - var icon string - iconColor := theme.Success - if isError { - icon = "×" - iconColor = theme.Error - } else { - icon = "✓" - } - - iconStr := lipgloss.NewStyle().Foreground(iconColor).Bold(true).Render(icon) - - // Extension can override display name. - displayName := toolDisplayName(toolName) - if extRd != nil && extRd.DisplayName != "" { - displayName = extRd.DisplayName - } - nameStr := lipgloss.NewStyle().Foreground(theme.Info).Bold(true).Render(displayName) - - // Format params — check extension renderer first. - paramBudget := max(r.width-10-len(displayName), 20) - var params string - if extRd != nil && extRd.RenderHeader != nil { - params = extRd.RenderHeader(toolArgs, paramBudget) - } - if params == "" { - params = formatToolParams(toolArgs, paramBudget) - } - - // Build header line - header := iconStr + " " + nameStr - if params != "" { - header += " " + lipgloss.NewStyle().Foreground(theme.Muted).Render(params) - } - - // Format body: check extension renderer first, then compact builtin, then default. - var body string - if extRd != nil && extRd.RenderBody != nil { - body = extRd.RenderBody(toolResult, isError, r.width-4) - // Apply markdown rendering if requested and body is non-empty. - if body != "" && extRd.BodyMarkdown { - body = strings.TrimSuffix(toMarkdown(body, r.width-4), "\n") - } - } - if body == "" { - if isError { - body = lipgloss.NewStyle().Foreground(theme.Error).Render(r.formatToolResult(toolResult)) - } else { - // Use compact summary renderers instead of full tool body renderers. - body = renderToolBodyCompact(toolName, toolArgs, toolResult, r.width-4) - if body == "" { - formatted := r.formatToolResult(toolResult) - if formatted == "" { - body = lipgloss.NewStyle().Foreground(theme.Muted).Italic(true).Render("(no output)") - } else { - body = lipgloss.NewStyle().Foreground(theme.Muted).Render(formatted) - } - } - } - } - - // Combine header + indented body - var lines []string - lines = append(lines, header) - if body != "" { - for line := range strings.SplitSeq(body, "\n") { - lines = append(lines, " "+line) - } - } - - return UIMessage{ - Type: ToolMessage, - Content: strings.Join(lines, "\n"), - Height: len(lines), - } -} - -// RenderSystemMessage renders a system notification or informational message in -// compact format with a distinctive symbol (*) and "System" label. Content is -// formatted to fit on a single line for minimal space usage. -func (r *CompactRenderer) RenderSystemMessage(content string, timestamp time.Time) UIMessage { - theme := GetTheme() - symbol := lipgloss.NewStyle().Foreground(theme.Muted).Render("◇") - label := lipgloss.NewStyle().Foreground(theme.Muted).Bold(true).Render("System") - - compactContent := r.formatCompactContent(content) - - line := fmt.Sprintf("%s %-8s %s", symbol, label, compactContent) - - return UIMessage{ - Type: SystemMessage, - Content: line, - Height: 1, - Timestamp: timestamp, - } -} - -// RenderErrorMessage renders an error notification in compact format with a -// distinctive error symbol (!) and styling to ensure visibility. The error -// content is displayed in a single line with appropriate color highlighting. -func (r *CompactRenderer) RenderErrorMessage(errorMsg string, timestamp time.Time) UIMessage { - theme := GetTheme() - symbol := lipgloss.NewStyle().Foreground(theme.Error).Render("!") - label := lipgloss.NewStyle().Foreground(theme.Error).Bold(true).Render("Error") - - compactContent := lipgloss.NewStyle().Foreground(theme.Error).Render(r.formatCompactContent(errorMsg)) - - line := fmt.Sprintf("%s %-8s %s", symbol, label, compactContent) - - return UIMessage{ - Type: ErrorMessage, - Content: line, - Height: 1, - Timestamp: timestamp, - } -} - -// RenderDebugMessage renders diagnostic information in compact format when debug -// mode is enabled. Messages are truncated if they exceed the available width to -// maintain single-line display. -func (r *CompactRenderer) RenderDebugMessage(message string, timestamp time.Time) UIMessage { - theme := GetTheme() - symbol := lipgloss.NewStyle().Foreground(theme.Tool).Render("*") - label := lipgloss.NewStyle().Foreground(theme.Tool).Bold(true).Render("Debug") - - // Truncate message if too long - content := message - if len(content) > r.width-20 { - content = content[:r.width-23] + "..." - } - - line := fmt.Sprintf("%s %-8s %s", symbol, label, content) - - return UIMessage{ - Type: SystemMessage, - Content: line, - Height: 1, - Timestamp: timestamp, - } -} - -// RenderDebugConfigMessage renders configuration settings in compact format for -// debugging purposes. Config entries are displayed as key=value pairs separated -// by commas, truncated if necessary to fit on a single line. -func (r *CompactRenderer) RenderDebugConfigMessage(config map[string]any, timestamp time.Time) UIMessage { - theme := GetTheme() - symbol := lipgloss.NewStyle().Foreground(theme.Tool).Render("*") - label := lipgloss.NewStyle().Foreground(theme.Tool).Bold(true).Render("Debug") - - // Format config as compact key=value pairs - var configPairs []string - for key, value := range config { - if value != nil { - configPairs = append(configPairs, fmt.Sprintf("%s=%v", key, value)) - } - } - - content := strings.Join(configPairs, ", ") - if len(content) > r.width-20 { - content = content[:r.width-23] + "..." - } - - line := fmt.Sprintf("%s %-8s %s", symbol, label, content) - - return UIMessage{ - Type: SystemMessage, - Content: line, - Height: 1, - Timestamp: timestamp, - } -} - -// formatCompactContent formats content for compact single-line display -func (r *CompactRenderer) formatCompactContent(content string) string { - if content == "" { - return "" - } - - // Remove markdown formatting for compact display - content = strings.ReplaceAll(content, "\n", " ") - content = strings.ReplaceAll(content, "\t", " ") - - // Collapse multiple spaces - for strings.Contains(content, " ") { - content = strings.ReplaceAll(content, " ", " ") - } - - content = strings.TrimSpace(content) - - // Truncate if too long (unless in debug mode) - maxLen := max( - // Reserve space for symbol and label more conservatively - r.width-28, - // Minimum width for readability - 40) - if !r.debug && len(content) > maxLen { - content = content[:maxLen-3] + "..." - } - - return content -} - -// formatUserAssistantContent formats user and assistant content using glamour markdown rendering -func (r *CompactRenderer) formatUserAssistantContent(content string) string { - if content == "" { - return "" - } - - // Calculate available width more conservatively - // Account for: symbol (1) + spaces (2) + label (up to 20 chars) + space (1) + margin (4) - availableWidth := max(r.width-28, - // Minimum width for readability - 40) - - // Use glamour to render markdown content with proper width - rendered := toMarkdown(content, availableWidth) - return strings.TrimSuffix(rendered, "\n") -} - -// wrapText wraps text to the specified width, preserving existing line breaks -func (r *CompactRenderer) wrapText(text string, width int) string { - if width <= 0 { - return text - } - - lines := strings.Split(text, "\n") - var wrappedLines []string - - for _, line := range lines { - if len(line) <= width { - wrappedLines = append(wrappedLines, line) - continue - } - - // Wrap long lines - words := strings.Fields(line) - if len(words) == 0 { - wrappedLines = append(wrappedLines, line) - continue - } - - currentLine := "" - for _, word := range words { - // If adding this word would exceed the width, start a new line - if len(currentLine)+len(word)+1 > width && currentLine != "" { - wrappedLines = append(wrappedLines, currentLine) - currentLine = word - } else { - if currentLine == "" { - currentLine = word - } else { - currentLine += " " + word - } - } - } - if currentLine != "" { - wrappedLines = append(wrappedLines, currentLine) - } - } - - return strings.Join(wrappedLines, "\n") -} - -// formatToolResult formats tool results preserving formatting but limiting to 5 lines -func (r *CompactRenderer) formatToolResult(result string) string { - if result == "" { - return "" - } - - // Check if this is bash output with stdout/stderr tags - if strings.Contains(result, "") || strings.Contains(result, "") { - result = r.formatBashOutput(result) - } - - // Calculate available width more conservatively - availableWidth := max(r.width-28, - // Minimum width for readability - 40) - - // First wrap the text to prevent long lines (tool results are usually plain text, not markdown) - wrappedResult := r.wrapText(result, availableWidth) - - // Then limit to 5 lines - lines := strings.Split(wrappedResult, "\n") - if len(lines) > 5 { - lines = lines[:5] - // Add truncation indicator - if len(lines) == 5 && lines[4] != "" { - lines[4] = lines[4] + "..." - } else { - lines = append(lines, "...") - } - } - - return strings.Join(lines, "\n") -} - -// formatBashOutput formats bash command output by removing stdout/stderr tags -// and styling appropriately. Delegates tag parsing to the shared parseBashOutput -// helper. -func (r *CompactRenderer) formatBashOutput(result string) string { - return parseBashOutput(result, GetTheme()) -} - -// UpdateTheme is a no-op for CompactRenderer since it fetches theme colors -// directly from GetTheme() in each rendering method. This stub satisfies -// the Renderer interface. -func (r *CompactRenderer) UpdateTheme() { - // No-op: theme colors are fetched fresh on each render -} diff --git a/internal/ui/factory.go b/internal/ui/factory.go index 0a7bef5c..36994876 100644 --- a/internal/ui/factory.go +++ b/internal/ui/factory.go @@ -25,7 +25,6 @@ type CLISetupOptions struct { Agent AgentInterface ModelString string Debug bool - Compact bool Quiet bool ShowDebug bool // Whether to show debug config ProviderAPIKey string // For OAuth detection @@ -76,7 +75,7 @@ func SetupCLI(opts *CLISetupOptions) (*CLI, error) { return nil, nil // No CLI in quiet mode } - cli, err := NewCLI(opts.Debug, opts.Compact) + cli, err := NewCLI(opts.Debug) if err != nil { return nil, fmt.Errorf("failed to create CLI: %v", err) } diff --git a/internal/ui/format.go b/internal/ui/format.go index 0032e2b7..84c47942 100644 --- a/internal/ui/format.go +++ b/internal/ui/format.go @@ -7,9 +7,8 @@ import ( "charm.land/lipgloss/v2" ) -// Renderer is the interface satisfied by both MessageRenderer and -// CompactRenderer. It allows model.go and cli.go to call rendering methods -// without branching on compact mode. +// Renderer is the interface satisfied by MessageRenderer. It allows model.go +// and cli.go to call rendering methods uniformly. type Renderer interface { RenderUserMessage(content string, timestamp time.Time) UIMessage RenderAssistantMessage(content string, timestamp time.Time, modelName string) UIMessage @@ -23,15 +22,14 @@ type Renderer interface { UpdateTheme() } -// Compile-time checks that both renderers satisfy the Renderer interface. +// Compile-time check that MessageRenderer satisfies the Renderer interface. var _ Renderer = (*MessageRenderer)(nil) -var _ Renderer = (*CompactRenderer)(nil) // parseBashOutput parses / tagged output from bash tool // results, styling stderr with the theme's error color. Returns the // combined, styled output string with tags stripped. // -// Shared by both MessageRenderer and CompactRenderer. +// Shared by MessageRenderer. func parseBashOutput(result string, theme Theme) string { var formattedResult strings.Builder remaining := result diff --git a/internal/ui/model.go b/internal/ui/model.go index 337db140..443c1034 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -243,9 +243,6 @@ type UIVisibility struct { // AppModelOptions holds configuration passed to NewAppModel. type AppModelOptions struct { - // CompactMode selects the compact renderer for message formatting. - CompactMode bool - // ModelName is the display name of the model (e.g. "claude-sonnet-4-5"). ModelName string @@ -428,15 +425,9 @@ type AppModel struct { // stream is the child streaming display component (spinner + streaming text). stream streamComponentIface - // renderer renders completed messages for tea.Println output. It is either - // a *MessageRenderer (standard mode) or a *CompactRenderer (compact mode), - // chosen at construction time via the Renderer interface. + // renderer renders completed messages for tea.Println output. renderer Renderer - // compactMode is retained for StreamComponent selection and any remaining - // mode-specific logic (e.g. startup info formatting). - compactMode bool - // modelName is the LLM model name shown in rendered messages. modelName string @@ -675,23 +666,14 @@ func NewAppModel(appCtrl AppController, opts AppModelOptions) *AppModel { height = 24 // sensible fallback } - // Choose the renderer implementation based on compact mode. - var rdr Renderer - if opts.CompactMode { - cr := NewCompactRenderer(width, false) - cr.getToolRenderer = opts.GetToolRenderer - rdr = cr - } else { - mr := newMessageRenderer(width, false) - mr.getToolRenderer = opts.GetToolRenderer - rdr = mr - } + mr := newMessageRenderer(width, false) + mr.getToolRenderer = opts.GetToolRenderer + rdr := mr m := &AppModel{ state: stateInput, appCtrl: appCtrl, renderer: rdr, - compactMode: opts.CompactMode, modelName: opts.ModelName, providerName: opts.ProviderName, loadingMessage: opts.LoadingMessage, @@ -764,7 +746,7 @@ func NewAppModel(appCtrl AppController, opts AppModelOptions) *AppModel { } } - m.stream = NewStreamComponent(opts.CompactMode, width, opts.ModelName) + m.stream = NewStreamComponent(width, opts.ModelName) m.stream.SetThinkingVisible(m.thinkingVisible) // If --resume was passed, open the session picker immediately. diff --git a/internal/ui/model_test.go b/internal/ui/model_test.go index 69e81c4e..be6fac04 100644 --- a/internal/ui/model_test.go +++ b/internal/ui/model_test.go @@ -132,7 +132,6 @@ func newTestAppModel(ctrl AppController) (*AppModel, *stubStreamComponent, *stub stream: stream, input: input, renderer: newMessageRenderer(80, false), - compactMode: false, modelName: "test-model", width: 80, height: 24, diff --git a/internal/ui/stream.go b/internal/ui/stream.go index 1a118413..f44f8e01 100644 --- a/internal/ui/stream.go +++ b/internal/ui/stream.go @@ -226,7 +226,7 @@ type StreamComponent struct { // from models that wrap reasoning in XML-like tags (Qwen, DeepSeek). inThinkTag bool - // renderer renders streaming assistant text in either compact or standard mode. + // renderer renders streaming assistant text. renderer Renderer // modelName is displayed in the streaming text header. @@ -247,17 +247,12 @@ type StreamComponent struct { } // NewStreamComponent creates a new StreamComponent ready to be embedded in AppModel. -func NewStreamComponent(compactMode bool, width int, modelName string) *StreamComponent { +func NewStreamComponent(width int, modelName string) *StreamComponent { if width == 0 { width = 80 } - var renderer Renderer - if compactMode { - renderer = NewCompactRenderer(width, false) - } else { - renderer = newMessageRenderer(width, false) - } + renderer := newMessageRenderer(width, false) return &StreamComponent{ spinnerFrames: knightRiderFrames(), diff --git a/internal/ui/tool_renderers.go b/internal/ui/tool_renderers.go index c22cc143..69f41aec 100644 --- a/internal/ui/tool_renderers.go +++ b/internal/ui/tool_renderers.go @@ -29,8 +29,7 @@ const ( ) // isShellTool reports if the tool name matches a shell-like tool (bash, grep, find, or -// tools with "shell"/"command" in the name). Used by both renderToolBody and -// renderToolBodyCompact to avoid code duplication. +// tools with "shell"/"command" in the name). Used by renderToolBody. func isShellTool(toolName string) bool { return toolName == "bash" || toolName == "grep" || toolName == "find" || strings.Contains(toolName, "shell") || strings.Contains(toolName, "command") @@ -738,183 +737,6 @@ func truncateLine(s string, maxWidth int) string { return xansi.Truncate(s, maxWidth, "…") } -// --------------------------------------------------------------------------- -// Compact tool body renderers — one-line summaries for compact mode -// --------------------------------------------------------------------------- - -// renderToolBodyCompact returns a brief summary string for tool results in -// compact display mode. Returns empty string to fall back to default. -func renderToolBodyCompact(toolName, toolArgs, toolResult string, width int) string { - switch { - case toolName == "edit": - return renderEditCompact(toolArgs, toolResult) - case toolName == "ls": - return renderLsCompact(toolResult) - case toolName == "read": - return renderReadCompact(toolResult) - case toolName == "write": - return renderWriteCompact(toolArgs) - case isShellTool(toolName): - return renderBashCompact(toolResult, width) - case toolName == "subagent": - return renderSubagentCompact(toolResult) - } - return "" -} - -// renderReadCompact returns a line-count summary for Read tool output. -func renderReadCompact(toolResult string) string { - content := strings.TrimSpace(toolResult) - if content == "" { - return "" - } - - lines := strings.Split(content, "\n") - - // Count actual code lines (those with "N: " line-number prefix) - codeLines := 0 - for _, line := range lines { - if idx := strings.Index(line, ": "); idx > 0 && idx <= 7 { - numPart := line[:idx] - if _, err := strconv.Atoi(strings.TrimSpace(numPart)); err == nil { - codeLines++ - } - } - } - if codeLines == 0 { - return "" - } - - theme := GetTheme() - summary := fmt.Sprintf("%d lines", codeLines) - return lipgloss.NewStyle().Foreground(theme.Muted).Italic(true).Render(summary) -} - -// renderEditCompact returns a change-count summary for Edit tool output. -func renderEditCompact(toolArgs, toolResult string) string { - var args map[string]any - if err := json.Unmarshal([]byte(toolArgs), &args); err != nil { - return "" - } - - oldText, _ := args["old_text"].(string) - newText, _ := args["new_text"].(string) - if oldText == "" && newText == "" { - return "" - } - - oldCount := len(strings.Split(oldText, "\n")) - newCount := len(strings.Split(newText, "\n")) - - theme := GetTheme() - var summary string - if oldCount == newCount { - summary = fmt.Sprintf("%d lines modified", oldCount) - } else { - summary = fmt.Sprintf("-%d/+%d lines", oldCount, newCount) - } - return lipgloss.NewStyle().Foreground(theme.Muted).Italic(true).Render(summary) -} - -// renderWriteCompact returns a line-count summary for Write tool output. -func renderWriteCompact(toolArgs string) string { - var args map[string]any - if err := json.Unmarshal([]byte(toolArgs), &args); err != nil { - return "" - } - - content, _ := args["content"].(string) - if content == "" { - return "" - } - - count := len(strings.Split(content, "\n")) - theme := GetTheme() - summary := fmt.Sprintf("%d lines written", count) - return lipgloss.NewStyle().Foreground(theme.Muted).Italic(true).Render(summary) -} - -// renderLsCompact returns an entry-count summary for Ls tool output. -func renderLsCompact(toolResult string) string { - content := strings.TrimSpace(toolResult) - if content == "" { - return "" - } - - entries := strings.Split(content, "\n") - theme := GetTheme() - summary := fmt.Sprintf("%d entries", len(entries)) - return lipgloss.NewStyle().Foreground(theme.Muted).Italic(true).Render(summary) -} - -// renderBashCompact returns the first few lines of bash output as a compact -// summary. Shows up to 3 meaningful output lines. -func renderBashCompact(toolResult string, width int) string { - result := strings.TrimSpace(toolResult) - if result == "" { - return "" - } - - lines := strings.Split(result, "\n") - - // Filter to meaningful output lines (skip STDERR: label, keep exit codes separate) - var outputLines []string - var exitCode string - inStderr := false - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed == "STDERR:" { - inStderr = true - continue - } - if strings.HasPrefix(trimmed, "Exit code:") { - exitCode = trimmed - continue - } - if trimmed == "" { - continue - } - outputLines = append(outputLines, line) - _ = inStderr // stderr lines are included in output - } - - if len(outputLines) == 0 { - if exitCode != "" { - theme := GetTheme() - return lipgloss.NewStyle().Foreground(theme.Error).Render(exitCode) - } - return "" - } - - const maxLines = 3 - theme := GetTheme() - - display := outputLines - if len(display) > maxLines { - display = display[:maxLines] - } - - // Truncate each line to available width (ANSI-aware) - lineMax := max(width-4, 20) - for i, line := range display { - display[i] = truncateLine(line, lineMax) - } - - summary := strings.Join(display, "\n") - if len(outputLines) > maxLines { - summary += fmt.Sprintf("\n...(%d more lines)", len(outputLines)-maxLines) - } - if exitCode != "" { - summary += "\n" + lipgloss.NewStyle().Foreground(theme.Error).Render(exitCode) - } - - return lipgloss.NewStyle().Foreground(theme.Muted).Render(summary) -} - -// --------------------------------------------------------------------------- -// Subagent tool renderers — show only summary, not full output -// --------------------------------------------------------------------------- - // renderSubagentBody renders a clean summary of subagent results with bash-style // background styling for consistency with other tools. func renderSubagentBody(toolResult string, width int) string { @@ -1026,27 +848,3 @@ func extractSubagentPreviewLines(content string, maxLines, maxWidth int) []strin return preview } - -// renderSubagentCompact returns a brief one-line summary for subagent results. -func renderSubagentCompact(toolResult string) string { - result := strings.TrimSpace(toolResult) - if result == "" { - return "" - } - - theme := GetTheme() - - // Extract just the first line which contains the status - lines := strings.Split(result, "\n") - if len(lines) == 0 { - return "" - } - - statusLine := lines[0] - - // Make it more compact by removing redundant words - statusLine = strings.Replace(statusLine, "Subagent completed successfully in ", "Completed in ", 1) - statusLine = strings.Replace(statusLine, "Subagent failed", "Failed", 1) - - return lipgloss.NewStyle().Foreground(theme.Muted).Italic(true).Render(statusLine) -}