all repos — llm_aggregator @ 4316d19f105d3ded269a393c048b38d2e83d3141

A CLI tool to aggregate RSS feeds and summarise them with LLMs

feat: add thinking tags toggle in TUI for LLM outputs

- Add thinking tag filtering to terminal UI display
  - Automatically remove `<think>...</think>` sections from summary by
default
  - Press `t` to toggle thinking content visibility (displayed in gray
at the head)
  - Add status indicator showing whether thinking is currently visible
(`ON`/`OFF`)
- Update `internal/tui/model.go` with toggle implementation
  - Add `showThinking`, `thinkingContent`, and `cleanSummary` fields to
`Model`
  - Extract thinking content and clean summary in `processingDoneMsg`
using regex
  - Implement `'t'` key handler to regenerate viewport content with or
without thinking
  - Add `wrapTextGray` for gray‑colored text and `stripThinkingTags`
helper
- Update `CHANGELOG.md` with version 0.8.0 entry describing the new
feature
- Fix formatting in `docs/TESTING.md` (remove extraneous line break in
important block)
Maxwell Jensen 85795372+maxwelljens@users.noreply.github.com
Thu, 23 Apr 2026 10:43:45 +0200
commit

4316d19f105d3ded269a393c048b38d2e83d3141

parent

51cde19dd83544f497e4ce82d27d3524433979b3

3 files changed, 106 insertions(+), 33 deletions(-)

jump to
M CHANGELOG.mdCHANGELOG.md

@@ -6,6 +6,17 @@ The format is based on [Keep a

Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.0] - 2026-04-23 + +### Added + +- **Thinking tags toggle in TUI**: LLMs that output `<think>`/`</think>` + thinking tags now have those sections automatically removed from the summary + display by default. Users can press `t` to toggle the thinking section back + on, displayed in gray at the head of the output with XML tags stripped for + cleaner presentation. A status indicator shows whether thinking is currently + visible. + ## [0.7.1] - 2026-04-22 ### Fixed
M docs/TESTING.mddocs/TESTING.md

@@ -64,11 +64,10 @@ 2. Environment variables (`LLM_AGGREGATOR_*`)

3. Config file (`~/.config/llm_aggregator/config.toml`) 4. Default values -> [!IMPORTANT] -> CLI arguments only override config file values when explicitly provided. If -> `--model` is not passed on the command line, the config file or environment -> variable value is used. > This prevents CLI defaults from overriding -> configuration file values. +> [!IMPORTANT] CLI arguments only override config file values when explicitly +> provided. If `--model` is not passed on the command line, the config file or +> environment variable value is used. This prevents CLI defaults from +> overriding configuration file values. ---
M internal/tui/model.gointernal/tui/model.go

@@ -3,6 +3,7 @@

import ( "context" "fmt" + "regexp" "strings" "time"

@@ -13,6 +14,9 @@ tea "github.com/charmbracelet/bubbletea"

"github.com/charmbracelet/lipgloss" "llm_aggregator/internal/runtime" ) + +// Regex pattern to match thinking tags (both <think> and </think>) +var thinkingTagRegex = regexp.MustCompile(`(?s)<think>.*?</think>`) const ( padding = 2

@@ -69,23 +73,26 @@

type progressMsg float64 type Model struct { - progress progress.Model - spinner spinner.Model - viewport viewport.Model - runtime *runtime.Runtime - currentStep int - totalSteps int - status string - subStatus string - startTime time.Time - elapsed time.Duration - width int - height int - done bool - errorMsg string - articlesCount int - processedCount int - showSummary bool + progress progress.Model + spinner spinner.Model + viewport viewport.Model + runtime *runtime.Runtime + currentStep int + totalSteps int + status string + subStatus string + startTime time.Time + elapsed time.Duration + width int + height int + done bool + errorMsg string + articlesCount int + processedCount int + showSummary bool + showThinking bool // Toggle state for thinking tags visibility + thinkingContent string // Extracted thinking content for toggle + cleanSummary string // Summary with thinking tags removed } func New(rt *runtime.Runtime) *Model {

@@ -100,15 +107,16 @@ sp.Spinner = spinner.Dot

sp.Style = lipgloss.NewStyle().Foreground(colorHighlight) return &Model{ - progress: prog, - spinner: sp, - runtime: rt, - currentStep: 0, - totalSteps: len(stepNames) - 1, // Last step is "Complete" - status: "Initialising...", - subStatus: "Loading configuration", - startTime: time.Now(), - showSummary: false, + progress: prog, + spinner: sp, + runtime: rt, + currentStep: 0, + totalSteps: len(stepNames) - 1, // Last step is "Complete" + status: "Initialising...", + subStatus: "Loading configuration", + startTime: time.Now(), + showSummary: false, + showThinking: false, // Default: thinking tags are hidden } }

@@ -147,6 +155,25 @@ }

switch msg.String() { case "q", "ctrl+c": return m, tea.Quit + case "t": + // Toggle thinking tags visibility + if m.thinkingContent != "" { + m.showThinking = !m.showThinking + contentWidth := m.viewport.Width + var summaryContent string + if m.showThinking { + // Show thinking at the head of the output (without XML tags) + thinkingText := stripThinkingTags(m.thinkingContent) + summaryContent = wrapTextGray("πŸ’­ Thinking\n\n"+thinkingText, contentWidth) + "\n\nπŸ“ Summary\n\n" + wrapText(m.cleanSummary, contentWidth) + } else { + // Hide thinking tags (default) + summaryContent = "πŸ“ Summary\n\n" + wrapText(m.cleanSummary, contentWidth) + } + m.viewport.SetContent(summaryContent) + // Reset scroll position to top when toggling + m.viewport.GotoTop() + return m, nil + } } case tea.WindowSizeMsg: m.width = msg.Width

@@ -204,12 +231,18 @@ m.subStatus = "Summary generated successfully!"

// Enable summary viewport if there's content to display if m.runtime.Summary != "" { m.showSummary = true + // Extract and remove thinking tags from the summary + m.thinkingContent = thinkingTagRegex.FindString(m.runtime.Summary) + m.cleanSummary = thinkingTagRegex.ReplaceAllString(m.runtime.Summary, "") + // Clean up extra whitespace left behind after removing thinking tags + m.cleanSummary = strings.TrimSpace(m.cleanSummary) // Resize viewport to fit available space m.viewport.Width = m.width - padding*2 m.viewport.Height = m.height - 15 // Leave space for header/footer // Wrap content to viewport width before setting contentWidth := m.viewport.Width - summaryContent := "πŸ“ Summary\n\n" + wrapText(m.runtime.Summary, contentWidth) + // Default: show summary without thinking tags + summaryContent := "πŸ“ Summary\n\n" + wrapText(m.cleanSummary, contentWidth) m.viewport.SetContent(summaryContent) } }

@@ -391,7 +424,16 @@ sb.WriteString(m.viewport.View())

sb.WriteString("\n") // Navigation help - sb.WriteString(infoStyle.Render("Navigate: ↑/↓ or j/k (scroll) | Space/B (page) | g/G (start/end) | q (quit)")) + var toggleIndicator string + if m.thinkingContent != "" { + if m.showThinking { + toggleIndicator = infoStyle.Render(" [πŸ’­ Thinking: ON]") + } else { + toggleIndicator = infoStyle.Render(" [πŸ’­ Thinking: OFF]") + } + } + sb.WriteString(infoStyle.Render("Navigate: ↑/↓ or j/k (scroll) | Space/B (page) | g/G (start/end) | t (toggle thinking) | q (quit)")) + sb.WriteString(toggleIndicator) return sb.String() }

@@ -401,3 +443,24 @@ func wrapText(text string, width int) string {

style := lipgloss.NewStyle().Width(width) return style.Render(text) } + +// wrapTextGray wraps text to a given width using lipgloss with gray colour. +func wrapTextGray(text string, width int) string { + style := lipgloss.NewStyle(). + Width(width). + Foreground(colorSubtle) + return style.Render(text) +} + +// stripThinkingTags removes the <think> and </think> XML tags from the thinking content. +func stripThinkingTags(content string) string { + // Remove <think> tag + reOpen := regexp.MustCompile(`<think>\s*`) + content = reOpen.ReplaceAllString(content, "") + + // Remove </think> tag + reClose := regexp.MustCompile(`\s*</think>`) + content = reClose.ReplaceAllString(content, "") + + return content +}