all repos — llm_aggregator @ 658b2cfd3cd0eca5835b921100e08569a426e199

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

feat: add markdown rendering and UI refinements to TUI

- Add Markdown rendering using `charmbracelet/glamour`
  - Render LLM summary output with proper styling (headers, bold,
    italic, code blocks, lists)
  - Re-render Markdown dynamically on terminal resize
- Formalise infobar keybinds as dedicated style variables
  - Keybinds bold, separators/actions subtle but distinguishable
- Move thinking toggle indicator to separate line above keybinds
  - Distinct styles: magenta+bold when ON, subtle+italic when OFF
- Implement dynamic text wrapping on terminal resize
  - Summary content re-wraps when window/font size changes
- Update documentation (README, man pages, CHANGELOG)
  - Document Markdown rendering in TUI mode
  - Add glamour to dependency list
  - Bump version to 0.9.0 and document changes in CHANGELOG
- Update dependencies (`go.mod`/`go.sum`)
  - Add `charm.land/glamour/v2`, `github.com/charmbracelet/glamour`
  - Update lipgloss, bubbles, and other charm libraries
Maxwell Jensen 85795372+maxwelljens@users.noreply.github.com
Thu, 23 Apr 2026 12:34:11 +0200
commit

658b2cfd3cd0eca5835b921100e08569a426e199

parent

64f25c518cd22ba98fb9ca131b0dd6d654958fbf

7 files changed, 227 insertions(+), 20 deletions(-)

jump to
M CHANGELOG.mdCHANGELOG.md

@@ -6,6 +6,24 @@ 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.9.0] - 2026-04-23 + +### Added + +- **Markdown rendering in TUI**: Implemented `charmbracelet/glamour` to + render LLM summary output with proper markdown styling. Headers, bold, + italic, code blocks, lists, and other markdown elements are now styled + for better readability in the terminal. +- **Formalised infobar keybinds**: TUI keybinds are now defined as + dedicated style variables at the top of the model for easier maintenance. + Keybinds are bold while separators and action text are subtle but + distinguishable. +- **Thinking indicator as separate element**: The `[đź’­ Thinking: ON/OFF]` + toggle indicator is now on its own line above the keybinds, using a + distinct style (magenta+bold when ON, subtle+italic when OFF). +- **Dynamic text wrapping on resize**: TUI summary text now re-wraps when + the terminal window size or font changes, with proper markdown re-rendering. + ## [0.8.0] - 2026-04-23 ### Added
M README.mdREADME.md

@@ -117,8 +117,10 @@ selected, the original articles can be included alongside the summary.

When the `--tui` flag is used, the entire process is wrapped in a `bubbletea` TUI that shows a colourful progress bar, live article counters, and elapsed -time. The TUI supports keyboard navigation (j/k, arrows, space, b, g/G) and -mouse wheel scrolling for browsing long summaries. +time. The TUI renders Markdown content from the LLM with proper styling for +headers, bold, italic, code blocks, and lists. The TUI supports keyboard +navigation (j/k, arrows, space, b, g/G) and mouse wheel scrolling for +browsing long summaries. Feeds are fetched concurrently for optimal performance, with rate limiting to avoid overwhelming feed servers.

@@ -212,6 +214,7 @@ | [`gofeed`](https://github.com/mmcdole/gofeed) | Robust RSS/Atom/JSON feed parser |

| [`openai‑go`](https://github.com/openai/openai-go) | Official OpenAI API library for Go | | [`bubbletea`](https://github.com/charmbracelet/bubbletea) | TUI framework for terminal applications | | [`lipgloss`](https://github.com/charmbracelet/lipgloss) | Library for styling terminal output | +| [`glamour`](https://github.com/charmbracelet/glamour) | Markdown rendering for terminal (used in TUI mode) | | [`go‑arg`](https://github.com/alexflint/go-arg) | Struct‑based argument parsing | | [`tiktoken-go`](https://github.com/pkoukk/tiktoken-go) | OpenAI's tiktoken BPE tokeniser | | [`viper`](https://github.com/spf13/viper) | Configuration management |
M docs/llm_aggregator.1docs/llm_aggregator.1

@@ -1,5 +1,5 @@

.\" Man page for llm_aggregator -.TH LLM_AGGREGATOR 1 "22 April 2026" "1.0.0" "User Commands" +.TH LLM_AGGREGATOR 1 "23 April 2026" "0.9.0" "User Commands" .SH NAME llm_aggregator \- aggregate RSS feeds and summarise them with LLMs .SH SYNOPSIS
M docs/llm_aggregator.5docs/llm_aggregator.5

@@ -1,5 +1,5 @@

.\" Man page for llm_aggregator configuration file -.TH LLM_AGGREGATOR 5 "22 April 2026" "1.0.0" "File Formats" +.TH LLM_AGGREGATOR 5 "23 April 2026" "0.9.0" "File Formats" .SH NAME llm_aggregator.conf \- configuration file for llm_aggregator .SH DESCRIPTION

@@ -23,6 +23,13 @@ The configuration file is a TOML file with the following sections and

options: .SH "[feed_aggregation]" Options controlling how feeds are fetched and processed. +.TP +.B feeds_file = \fI"string"\fR +Path to a file containing RSS feed URLs, one per line. This is the input +file that specifies which feeds to aggregate. +.br +Note: This is a required command\-line argument and cannot be set via +environment variable or config file. .TP .B max_articles_per_feed = \fIinteger\fR Maximum number of articles to fetch from each feed.

@@ -93,6 +100,13 @@ Default: 0.7

.SH "[system]" Options related to the LLM system prompt. .TP +.B prompt = \fI"string"\fR +User prompt for summarisation/analysis. This is a required argument that +instructs the LLM on what to do with the aggregated feed content. +.br +Note: This is a required command\-line argument and cannot be set via +environment variable or config file. +.TP .B system_prompt = \fI"string"\fR Custom system prompt for the LLM. The system prompt instructs the AI on how to behave and what format to use in its responses.

@@ -130,6 +144,32 @@ .B output

is set to "json". .br Default: false +.SH "[runtime]" +Options controlling runtime behaviour and output modes. +.TP +.B tui = \fIboolean\fR +Enable the terminal user interface (TUI) mode with a colourful progress +bar, live article counters, elapsed time, and a scrollable summary view. +When enabled, the program runs interactively with keyboard navigation +support (j/k, arrows, space, b, g/G for scrolling and q/Ctrl+C to quit). +Markdown content from the LLM is rendered with proper styling. +.br +Default: false +.TP +.B verbose = \fIboolean\fR +Enable verbose output mode. Shows detailed progress information including +feed fetching status, article filtering details, token usage estimates, +and LLM API interaction progress. +.br +Default: false +.TP +.B dry_run = \fIboolean\fR +Validate configuration and preview what would happen without making any +LLM API calls. Useful for validating the feeds file and configuration, +checking article counts and filtering results, and anything else that can +be done "offline". +.br +Default: false .SH EXAMPLE CONFIGURATION Below is a complete example configuration file: .PP

@@ -163,6 +203,11 @@ # Output options

output = "text" # Options: text, json, markdown # output_file = "" # Optional output file path include_articles = false + +# Runtime options +# tui = false # Enable TUI mode (default: false) +# verbose = false # Enable verbose output (default: false) +# dry_run = false # Validate config without API calls (default: false) .fi .RE .PP

@@ -195,6 +240,11 @@

# Output options output = "markdown" include_articles = true + +# Runtime options +tui = true # Enable TUI mode +verbose = true # Show progress details +dry_run = false # Perform actual LLM API call .fi .RE .SH CONFIGURATION PRECEDENCE

@@ -260,6 +310,12 @@ Output file path.

.TP .B LLM_AGGREGATOR_INCLUDE_ARTICLES Include articles in JSON output (true/false). +.TP +.B LLM_AGGREGATOR_DRY_RUN +Validate configuration without making LLM API calls (true/false). +.TP +.B LLM_AGGREGATOR_TUI +Enable TUI mode (true/false). .SH SECURITY CONSIDERATIONS .TP 2 \(bu
M go.modgo.mod

@@ -5,41 +5,53 @@

require golang.org/x/sync v0.20.0 require ( + charm.land/glamour/v2 v2.0.0 // indirect + charm.land/lipgloss/v2 v2.0.0 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/PuerkitoBio/goquery v1.8.0 + github.com/alecthomas/chroma/v2 v2.20.0 // indirect github.com/alexflint/go-arg v1.6.1 - github.com/mmcdole/gofeed v1.3.0 - github.com/openai/openai-go/v3 v3.32.0 github.com/alexflint/go-scalar v1.2.0 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/colorprofile v0.4.2 // indirect + github.com/charmbracelet/glamour v1.0.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/clipperhouse/displaywidth v0.9.0 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.5.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/mmcdole/gofeed v1.3.0 github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.16.0 // indirect + github.com/openai/openai-go/v3 v3.32.0 github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkoukk/tiktoken-go v0.1.8 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect

@@ -56,8 +68,11 @@ github.com/tidwall/match v1.1.1 // indirect

github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.7.13 // indirect + github.com/yuin/goldmark-emoji v1.0.6 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.39.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect )
M go.sumgo.sum

@@ -1,5 +1,11 @@

+charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U= +charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w= +charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo= +charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14= github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= +github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= github.com/alexflint/go-arg v1.6.1 h1:uZogJ6VDBjcuosydKgvYYRhh9sRCusjOvoOLZopBlnA= github.com/alexflint/go-arg v1.6.1/go.mod h1:nQ0LFYftLJ6njcaee0sU+G0iS2+2XJQfA8I062D0LGc= github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=

@@ -8,34 +14,56 @@ github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=

github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= +github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= +github.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08= +github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI= +github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=

@@ -49,6 +77,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=

github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=

@@ -61,8 +91,11 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=

github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4= github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE= github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 h1:Zr92CAlFhy2gL+V1F+EyIuzbQNbSgP4xhTODZtrXUtk=

@@ -76,6 +109,8 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=

github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/openai/openai-go/v3 v3.32.0 h1:aHp/3wkX1W6jB8zTtf9xV0aK0qPFSVDqS7AHmlJ4hXs=

@@ -87,6 +122,8 @@ github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=

github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=

@@ -122,6 +159,10 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=

github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= +github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=

@@ -137,7 +178,11 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
M internal/tui/model.gointernal/tui/model.go

@@ -7,6 +7,7 @@ "regexp"

"strings" "time" + "charm.land/glamour/v2" "github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/viewport"

@@ -58,6 +59,28 @@ "Getting summary",

"Formatting output", "Complete", } + + // Infobar keybinds - stylised for easy modification + infobarKeybindStyle = lipgloss.NewStyle(). + Foreground(colorSubtle). + Bold(true) + + infobarSeparatorStyle = lipgloss.NewStyle(). + Foreground(colorSubtle). + Italic(false) + + infobarActionStyle = lipgloss.NewStyle(). + Foreground(colorSubtle). + Italic(true) + + // Thinking toggle indicator (separate element above keybinds) + thinkingOnStyle = lipgloss.NewStyle(). + Foreground(colorHighlight). + Bold(true) + + thinkingOffStyle = lipgloss.NewStyle(). + Foreground(colorSubtle). + Italic(true) ) type StageMsg string

@@ -183,6 +206,16 @@ // Resize viewport for summary display

if m.showSummary { m.viewport.Width = msg.Width - padding*2 m.viewport.Height = msg.Height - 10 // Leave space for header/footer + // Re-wrap content to new width when resizing + contentWidth := m.viewport.Width + var summaryContent string + if m.showThinking && m.thinkingContent != "" { + thinkingText := stripThinkingTags(m.thinkingContent) + summaryContent = wrapTextGray("💭 Thinking\n\n"+thinkingText, contentWidth) + "\n\n📝 Summary\n\n" + wrapText(m.cleanSummary, contentWidth) + } else { + summaryContent = "📝 Summary\n\n" + wrapText(m.cleanSummary, contentWidth) + } + m.viewport.SetContent(summaryContent) } case progressMsg: // This message type seems unused, but leaving it in case.

@@ -423,25 +456,62 @@ // Viewport (scrollable summary)

sb.WriteString(m.viewport.View()) sb.WriteString("\n") - // Navigation help - var toggleIndicator string + // Thinking toggle indicator (separate element above keybinds) if m.thinkingContent != "" { if m.showThinking { - toggleIndicator = infoStyle.Render(" [💭 Thinking: ON]") + sb.WriteString(thinkingOnStyle.Render("[💭 Thinking: ON]")) } else { - toggleIndicator = infoStyle.Render(" [💭 Thinking: OFF]") + sb.WriteString(thinkingOffStyle.Render("[💭 Thinking: OFF]")) } + sb.WriteString("\n") } - sb.WriteString(infoStyle.Render("Navigate: ↑/↓ or j/k (scroll) | Space/B (page) | g/G (start/end) | t (toggle thinking) | q (quit)")) - sb.WriteString(toggleIndicator) + + // Infobar - stylised keybinds + sb.WriteString(infobarKeybindStyle.Render("↑/↓")) + sb.WriteString(infobarSeparatorStyle.Render(" or ")) + sb.WriteString(infobarKeybindStyle.Render("j/k")) + sb.WriteString(infobarActionStyle.Render(" (scroll)")) + sb.WriteString(infobarSeparatorStyle.Render(" | ")) + sb.WriteString(infobarKeybindStyle.Render("Space/B")) + sb.WriteString(infobarActionStyle.Render(" (page)")) + sb.WriteString(infobarSeparatorStyle.Render(" | ")) + sb.WriteString(infobarKeybindStyle.Render("g/G")) + sb.WriteString(infobarActionStyle.Render(" (start/end)")) + sb.WriteString(infobarSeparatorStyle.Render(" | ")) + sb.WriteString(infobarKeybindStyle.Render("t")) + sb.WriteString(infobarActionStyle.Render(" (toggle thinking)")) + sb.WriteString(infobarSeparatorStyle.Render(" | ")) + sb.WriteString(infobarKeybindStyle.Render("q")) + sb.WriteString(infobarActionStyle.Render(" (quit)")) return sb.String() } // wrapText wraps text to a given width using lipgloss. func wrapText(text string, width int) string { - style := lipgloss.NewStyle().Width(width) - return style.Render(text) + // Use glamour to render markdown with proper styling + r := getGlamourRenderer(width) + out, err := r.Render(text) + if err != nil { + // Fallback to plain text wrapping if glamour fails + style := lipgloss.NewStyle().Width(width) + return style.Render(text) + } + return out +} + +// getGlamourRenderer returns a cached glamour renderer configured for the given width. +func getGlamourRenderer(width int) *glamour.TermRenderer { + // Create a new renderer with the specified width + r, err := glamour.NewTermRenderer( + glamour.WithWordWrap(width), + glamour.WithStandardStyle("dark"), + ) + if err != nil { + // Fallback to default renderer if creation fails + r, _ = glamour.NewTermRenderer(glamour.WithWordWrap(width)) + } + return r } // wrapTextGray wraps text to a given width using lipgloss with gray colour.