Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
436 changes: 436 additions & 0 deletions .agents/plans/tui2-hardening/PLAN.md

Large diffs are not rendered by default.

568 changes: 568 additions & 0 deletions docs/architecture-review.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
charm.land/glamour/v2 v2.0.1
charm.land/lipgloss/v2 v2.0.5
github.com/atotto/clipboard v0.1.4
github.com/buchenberg/tviewmd v0.1.0
github.com/gdamore/tcell/v2 v2.8.1
github.com/lrstanley/bubblezone/v2 v2.0.0
github.com/lsferreira42/figlet-go v0.0.2-beta
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
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/buchenberg/tviewmd v0.1.0 h1:v0YXgD8PbJWiLCj1XrDhX6dgtUTnlFjd6QUz+Z8LnHQ=
github.com/buchenberg/tviewmd v0.1.0/go.mod h1:1ybuAmMTz5jZv3CJQ4OFtNkPtafVMgTcvGnC2vZvzQU=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
Expand Down
6 changes: 4 additions & 2 deletions internal/tui2/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ func (t *TUI2) HandleEvent(event agent.Event) {
case *agent.FlushEvent:
t.App.QueueUpdateDraw(func() {
if t.isStreaming.Load() && t.pendingTokens != "" {
t.addAssistantResponse(t.pendingTokens)
w := messageWidth(t.Messages)
t.addAssistantResponse(t.pendingTokens, w)
t.pendingTokens = ""
t.isStreaming.Store(false)
}
Expand Down Expand Up @@ -99,7 +100,8 @@ func (t *TUI2) HandleEvent(event agent.Event) {
case *agent.DoneEvent:
t.App.QueueUpdateDraw(func() {
if t.isStreaming.Load() && t.pendingTokens != "" {
t.addAssistantResponse(t.pendingTokens)
w := messageWidth(t.Messages)
t.addAssistantResponse(t.pendingTokens, w)
}
t.isStreaming.Store(false)
t.pendingTokens = ""
Expand Down
4 changes: 2 additions & 2 deletions internal/tui2/helpers_msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ func (t *TUI2) AddUserMessage(text string) {
}

// addAssistantResponse appends a markdown-rendered assistant response.
func (t *TUI2) addAssistantResponse(text string) {
t.appendMessage(renderMarkdown(text))
func (t *TUI2) addAssistantResponse(text string, width int) {
t.appendMessage(renderMarkdown(text, width))
}

// appendMessage adds raw text to the conversation log and refreshes.
Expand Down
59 changes: 21 additions & 38 deletions internal/tui2/markdown.go
Original file line number Diff line number Diff line change
@@ -1,49 +1,32 @@
package tui2

import (
"strings"
"sync"

"charm.land/glamour/v2"
"github.com/buchenberg/tviewmd"
"github.com/rivo/tview"
)

var (
rendererOnce sync.Once
renderer *glamour.TermRenderer
)

// initRenderer lazily creates a glamour renderer on first use. We use
// terminal256 formatting; the ANSI output is translated to tview color
// tags via tview.TranslateANSI at render time.
func initRenderer() {
rendererOnce.Do(func() {
r, err := glamour.NewTermRenderer(
glamour.WithStandardStyle("dark"),
glamour.WithWordWrap(80),
glamour.WithEmoji(),
glamour.WithChromaFormatter("terminal256"),
glamour.WithPreservedNewLines(),
)
if err == nil {
renderer = r
}
})
// renderMarkdown converts markdown to tview color-tagged text using the
// tviewmd native renderer (goldmark parser + tview tag backend). The
// output is suitable for a tview.TextView with SetWrap(true).
func renderMarkdown(md string, width int) string {
if md == "" {
return ""
}
if width <= 0 {
width = 80
}
return tviewmd.Render(md, tviewmd.Options{Width: width})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

Fix table sizing in the width-aware renderer.

renderMarkdown passes width to tviewmd, but tviewmd v0.1.0 calculates table columns from cell contents and does not use Options.Width. Wide tables are therefore wrapped by TextView, which breaks row alignment. (raw.githubusercontent.com)

This is the same unresolved issue from the previous review. Patch or upgrade the dependency, and add a narrow-width table regression test.

Verification script
#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

curl -fsSL \
  https://raw.githubusercontent.com/buchenberg/tviewmd/v0.1.0/render.go \
  -o "$tmpdir/render.go"
curl -fsSL \
  https://raw.githubusercontent.com/buchenberg/tviewmd/v0.1.0/render_tview.go \
  -o "$tmpdir/render_tview.go"

rg -n -C 3 'Width|func renderTable|opts\.Width' "$tmpdir"

This assessment uses the pinned tviewmd v0.1.0 implementation and the previous review finding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui2/markdown.go` at line 18, Fix table sizing in renderMarkdown by
upgrading or patching the pinned tviewmd dependency so table column widths honor
the supplied Options.Width instead of only cell-content widths. Add a regression
test covering a table rendered at a narrow width and verify rows remain aligned
without TextView wrapping.

}

// renderMarkdown converts markdown to tview color-tagged text using
// glamour for formatting and tview.TranslateANSI to convert the ANSI
// output into tview's native color tags. Falls back to the raw text if
// the renderer is unavailable.
func renderMarkdown(md string) string {
initRenderer()
if renderer == nil {
return md
// messageWidth returns a usable width for markdown rendering from the
// messages pane, defaulting to 80 if the pane hasn't been laid out yet.
func messageWidth(tv *tview.TextView) int {
if tv == nil {
return 80
}
out, err := renderer.Render(md)
if err != nil {
return md
_, _, w, _ := tv.GetInnerRect()
if w <= 0 {
return 80
}
out = strings.TrimSpace(out)
return tview.TranslateANSI(out)
return w
}
3 changes: 2 additions & 1 deletion internal/tui2/tui2.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,8 @@ func (t *TUI2) refreshMessages() {

// Streaming text (accumulated tokens, not yet flushed).
if t.isStreaming.Load() && t.pendingTokens != "" {
b.WriteString(renderMarkdown(t.pendingTokens))
w := messageWidth(t.Messages)
b.WriteString(renderMarkdown(t.pendingTokens, w))
b.WriteString("\n")
}

Expand Down
Loading