Skip to content

feat(ui): enhance color styling with adaptive colors and improve term…#22

Closed
josephgoksu wants to merge 2 commits intomainfrom
improvements/tui-and-tux
Closed

feat(ui): enhance color styling with adaptive colors and improve term…#22
josephgoksu wants to merge 2 commits intomainfrom
improvements/tui-and-tux

Conversation

@josephgoksu
Copy link
Owner

@josephgoksu josephgoksu commented Mar 8, 2026

…inal width handling

Greptile Summary

This PR upgrades the internal/ui package from hardcoded lipgloss.Color values to lipgloss.AdaptiveColor throughout, enabling proper light/dark terminal theme adaptation. It also introduces GetTerminalWidth() and auto-constraining column logic in Table.ColumnWidths(), migrates StyleBold to use lipgloss (gaining NO_COLOR support), widens the Panel.BorderColor API to accept any lipgloss.TerminalColor, and adds a "Why TaskWing?" performance comparison section to the README.

Key issues found:

  • styles.go (logic): The "note" category in CategoryBadge maps to ColorText as its badge background. In light mode ColorText.Light = "235" resolves to near-black (#262626), producing an inverted dark badge inside a light theme — semantically wrong and visually inconsistent with all other badge types.
  • table.go (logic): The new auto-constrain block in ColumnWidths() sums widths[i] values that are byte lengths (from len(h) / len(cell)) and compares them against the terminal column count from GetTerminalWidth(). For multi-byte Unicode content (emoji, CJK), the byte total overstates visual width, causing unnecessary and incorrect proportional shrinking. The padRight helper has the same len(s) issue; both should use lipgloss.Width() for visual measurement.
  • README.md (style): The "Real session, real numbers" benchmark figures (token counts, timing) are single-datapoint, environment-specific measurements presented without caveats, which may mislead users whose repositories or models produce different results.

Confidence Score: 3/5

  • Safe to merge for ASCII-only content, but has a light-mode visual regression for note badges and a measurement-unit bug that affects Unicode column sizing.
  • The adaptive color migration is directionally correct and the majority of changes are improvements. However, two concrete logic bugs were found: (1) ColorText used as a badge background produces a visually inverted dark badge in light mode, a UX regression, and (2) the new auto-constrain path in ColumnWidths() mixes byte counts with terminal column counts, silently producing incorrect column widths for any Unicode content. These are functional regressions introduced by this PR, not pre-existing issues.
  • internal/ui/styles.go (note badge background), internal/ui/table.go (byte-count vs column-count in auto-constrain), and internal/ui/spinner.go (hardcoded 80-char clear).

Important Files Changed

Filename Overview
internal/ui/styles.go Migrates all color constants from lipgloss.Color to lipgloss.AdaptiveColor for light/dark theme support; adds ColorPurple and ColorBarEmpty shared constants. ColorText is incorrectly reused as the "note" badge background, producing a near-black badge in light mode (a semantic inversion). ColorTableRowEven/ColorTableRowOdd remain declared but are still not applied in StyleTableRowEven/StyleTableRowOdd.
internal/ui/table.go Adds GetTerminalWidth() and a new auto-constrain block in ColumnWidths(). The constrain logic mixes byte-count widths (len(h), len(cell)) with terminal column count (GetTerminalWidth()), causing incorrect shrink ratios for Unicode content. The header/cell byte-count issue also affects padRight.
internal/ui/spinner.go Replaces the ANSI EL escape (\033[K) with a hardcoded 80-space clear. This regresses wide-terminal support and the strings import is added specifically for strings.Repeat.
internal/ui/context_view.go Replaces all hardcoded lipgloss.Color literals with adaptive color constants; changes scoreToColor return type from lipgloss.Color to lipgloss.TerminalColor. Inline lipgloss.AdaptiveColor{Light: "6", Dark: "14"} literal is duplicated across two render functions instead of being extracted to a named constant in styles.go.
internal/ui/config_menu.go All four config menu styles migrated to AdaptiveColor. configValueStyle now uses Light: "136" (goldenrod, ~#af8700) as text foreground; on a white terminal background this yields approximately 3:1 contrast, borderline for readability.
README.md Added a "Why TaskWing?" section with a comparison table and specific benchmark figures. Token counts and timing are presented as universally representative but are highly environment-specific; lacks reproducibility caveats.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Terminal Background Detection\nlipgloss.HasDarkBackground] --> B{Light or Dark?}
    B -->|Light| C[Resolve AdaptiveColor.Light]
    B -->|Dark| D[Resolve AdaptiveColor.Dark]

    C --> E[Named Color Constants\ne.g. ColorPrimary, ColorText, ColorWarning]
    D --> E

    E --> F[Package-Level Styles\nStyleTitle, StyleError, StyleSuccess ...]
    E --> G[In-Function Styles\nconfig_menu, context_view, spinner ...]
    E --> H[CategoryBadge\nnode type → AdaptiveColor background]

    F --> I[UI Components]
    G --> I
    H --> I

    I --> J[Table.Render]
    I --> K[Panel.Render]
    I --> L[Spinner.Start]
    I --> M[RenderContextResults]

    J --> N{MaxWidth set?}
    N -->|Yes| O[Apply MaxWidth cap per column]
    N -->|No| P[GetTerminalWidth via os.Stdout\nAuto-constrain column widths]
    P -->|byte-count total vs column-count available\n⚠️ Mixed units for Unicode| Q[Proportional shrink + min-4 clamp]
    O --> R[Render string output]
    Q --> R
Loading

Fix All in Claude Code

Last reviewed commit: 3a83f9b

Greptile also left 3 inline comments on this PR.

select {
case <-s.stop:
fmt.Fprint(os.Stderr, "\r\033[K")
fmt.Fprint(os.Stderr, "\r"+strings.Repeat(" ", 80)+"\r")
Copy link

Choose a reason for hiding this comment

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

Hardcoded 80-character clear regresses wide-terminal support.

The old implementation used the ANSI EL escape (\033[K), which clears the entire line regardless of terminal width. This replacement with strings.Repeat(" ", 80) is hardcoded to 80 characters—on terminals wider than 80 columns, spinner frames extending beyond column 80 will leave visible artifacts after the spinner stops.

Notably, GetTerminalWidth() was introduced in this same PR but is not used here. The fix is to either restore the ANSI escape or use the new helper:

Suggested change
fmt.Fprint(os.Stderr, "\r"+strings.Repeat(" ", 80)+"\r")
fmt.Fprint(os.Stderr, "\r\033[K")

Or alternatively:

Suggested change
fmt.Fprint(os.Stderr, "\r"+strings.Repeat(" ", 80)+"\r")
fmt.Fprint(os.Stderr, "\r"+strings.Repeat(" ", GetTerminalWidth())+"\r")

However, note that Comment on internal/ui/table.go line 133 flags that GetTerminalWidth() queries os.Stdout, not os.Stderr, so the second approach would require that helper to accept an *os.File parameter.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/ui/spinner.go
Line: 45

Comment:
Hardcoded 80-character clear regresses wide-terminal support.

The old implementation used the ANSI EL escape (`\033[K`), which clears the entire line regardless of terminal width. This replacement with `strings.Repeat(" ", 80)` is hardcoded to 80 characters—on terminals wider than 80 columns, spinner frames extending beyond column 80 will leave visible artifacts after the spinner stops.

Notably, `GetTerminalWidth()` was introduced in this same PR but is not used here. The fix is to either restore the ANSI escape or use the new helper:

```suggestion
fmt.Fprint(os.Stderr, "\r\033[K")
```

Or alternatively:
```suggestion
fmt.Fprint(os.Stderr, "\r"+strings.Repeat(" ", GetTerminalWidth())+"\r")
```

However, note that Comment on `internal/ui/table.go` line 133 flags that `GetTerminalWidth()` queries `os.Stdout`, not `os.Stderr`, so the second approach would require that helper to accept an `*os.File` parameter.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed — restored ANSI escape and removed unused strings import.

Comment on lines +57 to +68
if total > available {
// Proportionally shrink columns, but keep a minimum of 4 chars
ratio := float64(available) / float64(total)
for i := range widths {
newW := int(float64(widths[i]) * ratio)
if newW < 4 {
newW = 4
}
widths[i] = newW
}
}
}
Copy link

Choose a reason for hiding this comment

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

Minimum-clamp can still produce overflowing column totals.

After applying the proportional ratio (line 59), each column is floored to a minimum of 4 characters (lines 62–64). With many columns, the total 4*n + overhead can exceed termWidth, meaning the table will overflow even after the "constrain" pass. There is no post-clamp overflow check.

Example: termWidth=80, 15 columns, overhead=29, available=51, ratio≈0.34. Each newW = int(10*0.34)=3, clamped to 4. Post-clamp total = 4*15+29 = 89 > 80.

A follow-up validation is needed after the clamp loop to either re-trim the widest columns or document that overflow is accepted:

Suggested change
if total > available {
// Proportionally shrink columns, but keep a minimum of 4 chars
ratio := float64(available) / float64(total)
for i := range widths {
newW := int(float64(widths[i]) * ratio)
if newW < 4 {
newW = 4
}
widths[i] = newW
}
}
}
// After the minimum-clamp loop, check if post-clamp total still exceeds available
postClampTotal := 0
for _, w := range widths {
postClampTotal += w
}
if postClampTotal > available {
// Post-clamp overflow detected. Accept gracefully or trim.
// Consider logging a debug message or trimming extra from widest columns.
}

At minimum, add a comment documenting that the 4-char floor can cause overflow.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/ui/table.go
Line: 57-68

Comment:
Minimum-clamp can still produce overflowing column totals.

After applying the proportional `ratio` (line 59), each column is floored to a minimum of 4 characters (lines 62–64). With many columns, the total `4*n + overhead` can exceed `termWidth`, meaning the table will overflow even after the "constrain" pass. There is no post-clamp overflow check.

Example: `termWidth=80`, 15 columns, overhead=29, available=51, ratio≈0.34. Each `newW = int(10*0.34)=3`, clamped to `4`. Post-clamp total = `4*15+29 = 89 > 80`.

A follow-up validation is needed after the clamp loop to either re-trim the widest columns or document that overflow is accepted:

```suggestion
// After the minimum-clamp loop, check if post-clamp total still exceeds available
postClampTotal := 0
for _, w := range widths {
    postClampTotal += w
}
if postClampTotal > available {
    // Post-clamp overflow detected. Accept gracefully or trim.
    // Consider logging a debug message or trimming extra from widest columns.
}
```

At minimum, add a comment documenting that the 4-char floor can cause overflow.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed — added post-clamp overflow loop that trims widest columns until total fits within available width.

Comment on lines +133 to +138
w, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil || w <= 0 {
return 80
}
return w
}
Copy link

Choose a reason for hiding this comment

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

GetTerminalWidth() queries Stdout but may be used for output written to other streams.

GetTerminalWidth() calls term.GetSize(int(os.Stdout.Fd())), hard-coding the query to stdout. When Table.ColumnWidths() uses this function (line 48), the returned width assumes output will be written to stdout. However, a caller could render the table string and write it to stderr or another stream. In redirected pipelines (e.g., taskwing ask > output.txt), os.Stdout may not be a TTY while os.Stderr is a TTY, causing GetTerminalWidth() to return the fallback 80 even though the actual output stream is wider.

Consider accepting a parameter specifying which stream to query:

Suggested change
w, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil || w <= 0 {
return 80
}
return w
}
func GetTerminalWidthFor(f *os.File) int {
w, _, err := term.GetSize(int(f.Fd()))
if err != nil || w <= 0 {
return 80
}
return w
}
// GetTerminalWidth returns the current stdout terminal width, defaulting to 80.
func GetTerminalWidth() int {
return GetTerminalWidthFor(os.Stdout)
}

Then update Table.ColumnWidths() to accept an optional output stream parameter, or document that it assumes stdout.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/ui/table.go
Line: 133-138

Comment:
`GetTerminalWidth()` queries `Stdout` but may be used for output written to other streams.

`GetTerminalWidth()` calls `term.GetSize(int(os.Stdout.Fd()))`, hard-coding the query to stdout. When `Table.ColumnWidths()` uses this function (line 48), the returned width assumes output will be written to stdout. However, a caller could render the table string and write it to `stderr` or another stream. In redirected pipelines (e.g., `taskwing ask > output.txt`), `os.Stdout` may not be a TTY while `os.Stderr` is a TTY, causing `GetTerminalWidth()` to return the fallback `80` even though the actual output stream is wider.

Consider accepting a parameter specifying which stream to query:

```suggestion
func GetTerminalWidthFor(f *os.File) int {
	w, _, err := term.GetSize(int(f.Fd()))
	if err != nil || w <= 0 {
		return 80
	}
	return w
}

// GetTerminalWidth returns the current stdout terminal width, defaulting to 80.
func GetTerminalWidth() int {
	return GetTerminalWidthFor(os.Stdout)
}
```

Then update `Table.ColumnWidths()` to accept an optional output stream parameter, or document that it assumes stdout.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed — extracted GetTerminalWidthFor(f *os.File) that accepts any file descriptor. GetTerminalWidth() remains as a backward-compatible wrapper.

@josephgoksu
Copy link
Owner Author

@greptile

Comment on lines 129 to 133
badge := lipgloss.NewStyle().
Foreground(lipgloss.Color("0")).
Foreground(lipgloss.AdaptiveColor{Light: "231", Dark: "0"}).
Background(color).
Padding(0, 1).
Bold(true)
Copy link

Choose a reason for hiding this comment

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

Poor contrast: white text on warm light-mode badge backgrounds

In light mode, the badge foreground is set to "231" (effectively #ffffff white). For several badge background colors in light mode, this produces insufficient contrast:

  • ColorYellow.Light = "136" → approximately #af8700 (goldenrod). White text on goldenrod ≈ 2.8:1 contrast ratio — fails WCAG AA (requires 4.5:1 for normal text).
  • ColorWarning.Light = "172" → approximately #d78700 (amber/orange). White text on amber ≈ 3.1:1 — also fails.
  • ColorText.Light = "235" → approximately #262626 (near-black). White text on near-black is fine (high contrast), but this will be invisible to users.

The dark-mode pairing ("0" = black on vivid colors) is generally fine, but the light-mode foreground of "231" is the wrong direction. On a light terminal background the badge background colors are mid-toned; the readable foreground choice should be dark (e.g., "0" or "232"), not white.

Suggested change
badge := lipgloss.NewStyle().
Foreground(lipgloss.Color("0")).
Foreground(lipgloss.AdaptiveColor{Light: "231", Dark: "0"}).
Background(color).
Padding(0, 1).
Bold(true)
badge := lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "0", Dark: "0"}).
Background(color).
Padding(0, 1).
Bold(true)

If you need a lighter foreground on dark badge backgrounds in dark mode, consider using {Light: "0", Dark: "231"} (dark text in light mode, white text in dark mode), which inverts the current (broken) mapping.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/ui/styles.go
Line: 129-133

Comment:
**Poor contrast: white text on warm light-mode badge backgrounds**

In light mode, the badge foreground is set to `"231"` (effectively `#ffffff` white). For several badge background colors in light mode, this produces insufficient contrast:

- `ColorYellow.Light = "136"` → approximately `#af8700` (goldenrod). White text on goldenrod ≈ 2.8:1 contrast ratio — fails WCAG AA (requires 4.5:1 for normal text).
- `ColorWarning.Light = "172"` → approximately `#d78700` (amber/orange). White text on amber ≈ 3.1:1 — also fails.
- `ColorText.Light = "235"` → approximately `#262626` (near-black). White text on near-black is fine (high contrast), but this will be invisible to users.

The dark-mode pairing (`"0"` = black on vivid colors) is generally fine, but the light-mode foreground of `"231"` is the wrong direction. On a light terminal background the badge background colors are mid-toned; the readable foreground choice should be dark (e.g., `"0"` or `"232"`), not white.

```suggestion
	badge := lipgloss.NewStyle().
		Foreground(lipgloss.AdaptiveColor{Light: "0", Dark: "0"}).
		Background(color).
		Padding(0, 1).
		Bold(true)
```

If you need a lighter foreground on dark badge backgrounds in dark mode, consider using `{Light: "0", Dark: "231"}` (dark text in light mode, white text in dark mode), which inverts the current (broken) mapping.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed — badge foreground changed from Light:231 (white) to Light:0 (black) for proper contrast on light-mode backgrounds.

Comment on lines +91 to +92
ColorTableRowEven = lipgloss.AdaptiveColor{Light: "254", Dark: "236"} // Subtle background
ColorTableRowOdd = lipgloss.AdaptiveColor{Light: "231", Dark: "234"} // Slightly different
Copy link

Choose a reason for hiding this comment

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

ColorTableRowEven / ColorTableRowOdd are dead constants

These two color variables are declared but have zero usages anywhere in the codebase (verified with a project-wide search). StyleTableRowEven and StyleTableRowOdd reference ColorText and ColorDim respectively — not these variables. They appear to be legacy background-color hints that were never wired up.

Either:

  1. Apply them as Background(...) colors in StyleTableRowEven/StyleTableRowOdd, or
  2. Remove the declarations to avoid confusion.

Keeping unreferenced exported color constants inflates the public surface of the ui package and misleads readers into thinking alternating-row background coloring is already implemented.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/ui/styles.go
Line: 91-92

Comment:
**`ColorTableRowEven` / `ColorTableRowOdd` are dead constants**

These two color variables are declared but have zero usages anywhere in the codebase (verified with a project-wide search). `StyleTableRowEven` and `StyleTableRowOdd` reference `ColorText` and `ColorDim` respectively — not these variables. They appear to be legacy background-color hints that were never wired up.

Either:
1. Apply them as `Background(...)` colors in `StyleTableRowEven`/`StyleTableRowOdd`, or
2. Remove the declarations to avoid confusion.

Keeping unreferenced exported color constants inflates the public surface of the `ui` package and misleads readers into thinking alternating-row background coloring is already implemented.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed — removed unused ColorTableRowEven and ColorTableRowOdd declarations.

Comment on lines +46 to +51
// Auto-constrain to terminal width when MaxWidth is not set
if t.MaxWidth == 0 {
termWidth := GetTerminalWidth()
// Account for leading space + column separators (2 chars between each column)
overhead := 1 + (len(widths)-1)*2
available := termWidth - overhead
Copy link

Choose a reason for hiding this comment

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

Overhead formula underestimates when len(widths) == 0

When t.Headers is empty the guard in Render() returns early, so ColumnWidths() will never reach this block in the current code. However, ColumnWidths() is a public method and can be called directly on a zero-header Table. In that case:

overhead = 1 + (0 - 1) * 2  →  1 + (-2)  →  -1
available = termWidth - (-1)  →  termWidth + 1

available > 0 is satisfied but the total loop over an empty slice produces 0 > available → false, so no crash occurs. Still, -1 overhead is semantically wrong and makes the guard appear broader than it is. A minimal defensive fix:

Suggested change
// Auto-constrain to terminal width when MaxWidth is not set
if t.MaxWidth == 0 {
termWidth := GetTerminalWidth()
// Account for leading space + column separators (2 chars between each column)
overhead := 1 + (len(widths)-1)*2
available := termWidth - overhead
// Account for leading space + column separators (2 chars between each column)
overhead := 1
if len(widths) > 1 {
overhead += (len(widths) - 1) * 2
}
available := termWidth - overhead
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/ui/table.go
Line: 46-51

Comment:
**Overhead formula underestimates when `len(widths) == 0`**

When `t.Headers` is empty the guard in `Render()` returns early, so `ColumnWidths()` will never reach this block in the current code. However, `ColumnWidths()` is a public method and can be called directly on a zero-header `Table`. In that case:

```
overhead = 1 + (0 - 1) * 2  →  1 + (-2)  →  -1
available = termWidth - (-1)  →  termWidth + 1
```

`available > 0` is satisfied but the `total` loop over an empty slice produces `0 > available` → false, so no crash occurs. Still, `-1` overhead is semantically wrong and makes the guard appear broader than it is. A minimal defensive fix:

```suggestion
		// Account for leading space + column separators (2 chars between each column)
		overhead := 1
		if len(widths) > 1 {
			overhead += (len(widths) - 1) * 2
		}
		available := termWidth - overhead
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed — overhead formula now guards against len(widths) <= 1 to avoid negative overhead.

…ling

- Restore ANSI escape for spinner line clearing instead of hardcoded 80-char width
- Add post-clamp overflow redistribution in table column sizing
- Extract GetTerminalWidthFor(f *os.File) to support non-stdout streams
- Fix badge foreground contrast for light-mode terminals (white -> black)
- Remove unused ColorTableRowEven/ColorTableRowOdd declarations
- Guard overhead formula against empty widths slice
- Move Supported Models section up in README
@josephgoksu
Copy link
Owner Author

@greptile

"plan": ColorSuccess,
"note": ColorText,
"metadata": ColorCyan,
"documentation": ColorYellow,
Copy link

Choose a reason for hiding this comment

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

ColorText misused as badge background for "note" type

ColorText is defined as {Light: "235", Dark: "252"}. In light-mode terminals, "235" resolves to approximately #262626 (near-black). Using a foreground-text color as a badge background causes a visual inversion in light mode: a "note" badge will render with a near-black background, which is jarring and semantically wrong when every other UI element in that theme is light.

In the old code this was lipgloss.Color("252") — a consistent light-gray in all modes. The new mapping produces:

  • Dark mode: "252" (#d0d0d0, light gray) background — visually light and subtle, fine.
  • Light mode: "235" (#262626, near-black) background — visually an inverted/dark badge in a light theme, inconsistent with all other badge backgrounds.

Consider introducing a dedicated neutral adaptive color for "note" badges, e.g.:

// in styles.go
ColorNeutral = lipgloss.AdaptiveColor{Light: "248", Dark: "252"} // Neutral gray for note badges

Then in CategoryBadge:

Suggested change
"documentation": ColorYellow,
"note": ColorNeutral,
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/ui/styles.go
Line: 119

Comment:
**`ColorText` misused as badge background for "note" type**

`ColorText` is defined as `{Light: "235", Dark: "252"}`. In light-mode terminals, `"235"` resolves to approximately `#262626` (near-black). Using a foreground-text color as a badge *background* causes a visual inversion in light mode: a "note" badge will render with a near-black background, which is jarring and semantically wrong when every other UI element in that theme is light.

In the old code this was `lipgloss.Color("252")` — a consistent light-gray in all modes. The new mapping produces:
- **Dark mode:** `"252"` (#d0d0d0, light gray) background — visually light and subtle, fine.
- **Light mode:** `"235"` (#262626, near-black) background — visually an inverted/dark badge in a light theme, inconsistent with all other badge backgrounds.

Consider introducing a dedicated neutral adaptive color for "note" badges, e.g.:

```go
// in styles.go
ColorNeutral = lipgloss.AdaptiveColor{Light: "248", Dark: "252"} // Neutral gray for note badges
```

Then in `CategoryBadge`:
```suggestion
		"note":          ColorNeutral,
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Comment on lines +53 to +56
}
available := termWidth - overhead
if available > 0 {
total := 0
Copy link

Choose a reason for hiding this comment

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

Mixed byte-count vs. terminal-column comparison in auto-constrain logic

widths[i] values are populated using len(h) (header) and len(cell) (row cells), which return byte lengths. GetTerminalWidth() returns terminal columns (visual width). For pure ASCII these are equivalent, but multi-byte UTF-8 content (e.g., emoji in headers like "🔍 Symbol" used in explain.go, or CJK characters) will cause total (bytes) to exceed available (columns) even when the table would visually fit, triggering unnecessary proportional shrinking. In extreme cases with wide characters, columns get over-shrunk and the table renders narrower than the terminal.

The fix is to measure visual width using lipgloss.Width(s) (which accounts for Unicode and ANSI sequences) when calculating widths:

// Instead of len(h) / len(cell), use:
import "github.com/charmbracelet/lipgloss"

// In ColumnWidths():
for i, h := range t.Headers {
    widths[i] = lipgloss.Width(h)
}
for _, row := range t.Rows {
    for i, cell := range row {
        if i < len(widths) && lipgloss.Width(cell) > widths[i] {
            widths[i] = lipgloss.Width(cell)
        }
    }
}

padRight has the same len(s) issue — it should also use lipgloss.Width(s) to avoid producing visually misaligned padding for Unicode content.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/ui/table.go
Line: 53-56

Comment:
**Mixed byte-count vs. terminal-column comparison in auto-constrain logic**

`widths[i]` values are populated using `len(h)` (header) and `len(cell)` (row cells), which return **byte lengths**. `GetTerminalWidth()` returns **terminal columns** (visual width). For pure ASCII these are equivalent, but multi-byte UTF-8 content (e.g., emoji in headers like `"🔍 Symbol"` used in `explain.go`, or CJK characters) will cause `total` (bytes) to exceed `available` (columns) even when the table would visually fit, triggering unnecessary proportional shrinking. In extreme cases with wide characters, columns get over-shrunk and the table renders narrower than the terminal.

The fix is to measure visual width using `lipgloss.Width(s)` (which accounts for Unicode and ANSI sequences) when calculating `widths`:

```go
// Instead of len(h) / len(cell), use:
import "github.com/charmbracelet/lipgloss"

// In ColumnWidths():
for i, h := range t.Headers {
    widths[i] = lipgloss.Width(h)
}
for _, row := range t.Rows {
    for i, cell := range row {
        if i < len(widths) && lipgloss.Width(cell) > widths[i] {
            widths[i] = lipgloss.Width(cell)
        }
    }
}
```

`padRight` has the same `len(s)` issue — it should also use `lipgloss.Width(s)` to avoid producing visually misaligned padding for Unicode content.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Comment on lines +50 to +56
```

**Real session, real numbers** — asked *"What are the bottlenecks in our engineering process?"*:
- **Without TaskWing:** 8 Glob/Grep searches, 12 file reads, 25,000 tokens, 3 minutes
- **With TaskWing MCP:** 1 query, 1,500 tokens, 42 seconds — synthesized answer with code references

That's **90% fewer tokens** and **75% faster** time-to-answer.
Copy link

Choose a reason for hiding this comment

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

Highly specific benchmark claims lack reproducibility caveats

The figures 25,000 tokens, 1,500 tokens, 42 seconds, and 3 minutes are labeled "Real session, real numbers" but will vary significantly by repository size, AI model, query complexity, network latency, and TaskWing knowledge-base completeness. Presenting single-datapoint figures as representative benchmarks without any caveat could mislead users whose results differ substantially.

Consider adding a qualifier such as:

Suggested change
```
**Real session, real numbers** — asked *"What are the bottlenecks in our engineering process?"*:
- **Without TaskWing:** 8 Glob/Grep searches, 12 file reads, 25,000 tokens, 3 minutes
- **With TaskWing MCP:** 1 query, 1,500 tokens, 42 seconds — synthesized answer with code references
That's **90% fewer tokens** and **75% faster** time-to-answer.
**Real session, real numbers** *(results vary by repository size and model)* — asked *"What are the bottlenecks in our engineering process?"*:

Or add a footnote explaining the test environment (repo LOC, model used, knowledge-base node count).

Prompt To Fix With AI
This is a comment left during a code review.
Path: README.md
Line: 50-56

Comment:
**Highly specific benchmark claims lack reproducibility caveats**

The figures `25,000 tokens`, `1,500 tokens`, `42 seconds`, and `3 minutes` are labeled "Real session, real numbers" but will vary significantly by repository size, AI model, query complexity, network latency, and TaskWing knowledge-base completeness. Presenting single-datapoint figures as representative benchmarks without any caveat could mislead users whose results differ substantially.

Consider adding a qualifier such as:

```suggestion
**Real session, real numbers** *(results vary by repository size and model)* — asked *"What are the bottlenecks in our engineering process?"*:
```

Or add a footnote explaining the test environment (repo LOC, model used, knowledge-base node count).

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant