TUI rendering (zeroline): glamour markdown, syntax highlighting, themed surfaces#114
Conversation
…ed surfaces Module of the runtime-core split (off main). Renamed the reskin package from the working name 'zenline' to **zeroline** (dropping the zenline name; this becomes ZERO's default polished terminal surface). Scope: internal/zeroline only — pure presentation, no internal deps. - glamour-rendered markdown (with a bounded mdCache to avoid per-frame re-render cost) and chroma syntax highlighting (transitive via glamour). - colored diffs, full-bleed background, a Zen home page + a vim/powerline-style statusline chat page sharing 5 switchable color themes (theme.go). - callers (the TUI model) build the data structs from live agent state; this package turns them into styled frames. It is FORWARD code — wired by the TUI module in a later PR; exercised here by render/markdown tests. Deps: adds github.com/charmbracelet/glamour v1.0.0 (pulls chroma/v2, goldmark, bluemonday, x/net, x/term transitively; lipgloss/bubbletea/termenv already present). go.sum updated via go mod tidy; build/vet/-race/full-suite green; no behavior change to existing packages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Zero automated PR reviewVerdict: No blockers found Blockers
Validation
ScopeHead: This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality. |
anandh8x
left a comment
There was a problem hiding this comment.
What's good
-
Bounded caches that prevent a resize drag from blowing out memory.
mdOutCacheMax = 512andmdCacheMax = 256are constants with explicit comments: "a resize drag (many widths) can't grow it forever" and "a resize mints a width key per frame".TestMarkdownRendererCacheBoundedexercises the cap by insertingmdCacheMax * 3width keys and asserts the cache never exceeds the bound. The reset is a simplemdCache = map[mdKey]...{}("cheap to repopulate") — that's the right call for a key-space that is at mostvariants * 2 * widths, well below the cap in practice. -
Cache key includes
variant, dark, widthso theme/mode/resize all get a fresh renderer. The comment is explicit: "A renderer bakes in the theme colors AND the word-wrap width, so any change to variant, mode or width needs a fresh one."TestMarkdownRendererCachedPerKeycovers all three axes (identical key returns same pointer; width change gets a new one; variant change gets a new one). This is the right call — glamour's renderer bakes the colors and the wrap width in. -
Non-reentrant mutex handling around the cache.
renderMarkdownreleasesmdMubefore callingmarkdownRenderer(which takes the same mutex), with a comment that sayssync.Mutexis not reentrant. The pattern islookup → unlock → render → re-lock → insertso the slow glamour render runs without the lock held. The same pattern protects the output cache. Correct. -
Lockstep guarantee between
PermLayoutandpermModalLines.permModalLinesbuilds exactlypermModalRows = 8lines with the buttons onpermBtnRow = 5, and panics on drift:panic("permModalLines: modal line count drifted from permModalRows"). The constants live next to each other and the comment names the relationship: "PermLayout places the button hitboxes assuming this exact shape, so keep the count and the buttons row in lockstep with the constants."PermLayoutre-applies the same width/height clamps asRenderChat(floors to 40 wide and 8 tall, bodyH = height-3 floored to 1), so a sub-floor size still produces a hitbox that lands on the actual button row. -
Permission hitbox correctly disabled when buttons would be clipped.
PermLayoutreturnsActive: falsewhentop + permBtnRow >= bodyH, with a comment: "the modal is clipped above the buttons — disable the hitboxes so a click can't resolve to allow/deny on a row where no button is drawn."TestPermLayoutDisablesHitboxesWhenButtonsClippedexercises this atheight=8(where bodyH=5 < top+5). Without this guard a click on a non-button row would silently resolve to allow/deny. -
Permission hitbox disabled at sub-floor sizes.
TestPermLayoutMatchesRenderClampedrunsPermLayoutandRenderChatat sizes{10,11}, {20,12}, {30,14}, {38,24}— all below the 40-wide floor — and asserts three things: (1) the button row contains both "allow" and "deny" labels, (2) the rendered "[ a" bracket sits at exactlyg.Allow.X + 2columns, (3) no other row carries both labels. The comment names the bug this guards: "raw width shifts the box by a column at sub-40 widths and the click would miss." -
Sub-floor-height clamp is tested explicitly.
TestPermLayoutMatchesRenderClampedthen runsPermLayout(50, h)forh ∈ {1, 4, 7}and assertsg.Allow.Y < frameH. The comment names the threat: "the hitbox never points past the frame." -
Overlay is suppressed when a permission prompt is up.
overlayRegionreturns""whend.Perm != nil(ormaxRows <= 0), with a comment: "PermLayout (the mouse hit-test) lays the modal out assuming no overlay rows, so showing one here would drift the rendered buttons away from their hitboxes."TestPermLayoutMatchesRenderWithOverlayPresentconfirms that an active picker + suggestions overlay is NOT rendered while a permission prompt is up, and the frame is still exactlyhrows tall. -
Frame height is invariant under overlay cap.
TestOverlayCappedKeepsFrameHeight(40 suggestions, frame height 16) andTestPickerOverlayCappedKeepsFrameHeight(50 items, frame height 14) both assertlen(lines) == hAND a "… N more" summary row is present.capRowsreserves a row for the summary when it has to truncate, so the visible count leaves room for it (visible + 1 <= maxRows). A non-positive budget shows nothing. -
Clip budgets by display width, not rune count.
clipcallslipgloss.Widthto measure andansi.Truncateto clip, so wide runes (CJK = 2 cells, emoji = 2 cells) never exceed the budget. The comment names the threat: "a naive rune-count clip would let the line exceed its width budget."TestClipBudgetsByDisplayWidthcovers 3 inputs (世× 20,🚀× 15, mixed) × 4 widths (4/8/12/20) and assertslipgloss.Width(got) <= wfor every combination. Excellent coverage. -
Glamour output is clipped per line.
renderAssistantMarkdowncallsansi.Truncate(ln, clipW, "…")on every line. The comment names the threat: "glamour word-wraps at wrapW but does NOT hard-break unbreakable tokens (a long URL or fenced-code line), so it can emit a line far wider than the frame. Clip each output line to the budget … with an ANSI-aware truncate so styled escapes stay intact."TestAssistantMarkdownClipsLongLines(300-char URL) andTestAssistantMarkdownClipsLongFencedCode(300-charxline) assert every emitted line fits the budget. A real bug — without the clip, a single long line would blow out the fixed-height/full-bleed layout. -
Tool result collapsing is the right default.
resultSummaryper-tool:read_file→ "N lines" (never dumps content),list_directory→ "N entries",glob→ "N matches",grep→ "0 matches" or "N matches" with body cap of 4 lines,edit_file/apply_patch→ show body (diffs) with cap 8,bash→ show body with cap 10, errors → cap 3 of the error body.TestToolResultRenderingCollapsesAndShowsasserts each path:read_fileshows "217 lines" and never the raw content;list_directoryshows "3 entries";edit_fileshows the diff; errors show the failure text. This is the right summary-vs-body policy for a coding agent transcript. -
Diff colorization is bounded.
diffMaxLines = 40is the cap; long diffs surface a "… N more lines" footer. The cap protects the transcript from huge patches.TestColorizeDiffCapsLongDiffswrites 100 added lines and asserts the rendered output has a footer andlines <= diffMaxLines + 2. ThecolorizeDiffhelper is exported with a clean(detail, p Pal) stringsignature for callers outside the package. -
Colorized diff uses theme Red/Green, not a literal color.
diffLineswitches on the leading marker (@@/+++/---/diff/index→ mute,+→ green,-→ red, default → dim).TestColorizeDiffColorsAddsAndDelsuseslipgloss.NewStyle().Foreground(p.Green).Render("+added line")and asserts the rendered output contains that exact string, so the test is theme-aware (if a theme'sGreenwere the same as itsRedthe test pre-fails with a clear message). The theme is the source of truth for the colors. -
Theme structure is clean and small.
Themehas Name, Swt, Dark, Light;Palhas 14 fields (Bg, Panel, Panel2, Fg, Dim, Mute, Line, Line2, Accent, Accent2, Green, Amber, Red, Sel).mkPaltakes 14 hex strings and returns aPal.Resolveclamps the variant into bounds. The 5 themes (Phosphor/Cyan/Sage/Violet/Mono) each have distinct dark/light palettes.TestThemesCountassertslen(Themes) == 5and that each has a name and both Bg colors set;TestRenderHomeAllThemesrenders the home page in each. -
Boot splash is a thoughtful micro-animation.
RenderBootreveals the ZERO wordmark line-by-line with explicitreveal = []int{1,3,5,7,9}("per-line reveal frames ~120ms each"), then the tagline at frame 11, then the loading spinner at frame 8. The wordmark is rendered as 5 lines of fixed-width ASCII art. Pure presentation; no state. -
Statusline top bar / bottom bar have a clean three-section layout.
topBarshowsmode + branch + cwd + model + provon the left andprov + ctx + coston the right, with abar()helper that fills the gap with aPanel-colored spacer.botBarshowsmode + utf-8 + tok/son the left andctx-gauge + theme-name + theme-hinton the right. The gauge is a 8-cell▕██████░░▏bar. -
Run-state derivation is a clean switch.
RenderChatderivesrunfromPerm/AskUser(blocked) →Working(work) →hasAssistant(rows)(done) →normal. The mode is rendered as a colored pill (⟳ WORKINGamber,✓ DONEgreen,⚠ BLOCKEDred,NORMALaccent) in the top bar. Clear state machine. -
Ask-user takes the place of the thinking spinner. The transcript switch renders
askUserLines(a, tw)whend.AskUser != niland the comment says: "A pending questionnaire takes the place of the thinking/streaming line: show the focused question, not a misleading spinner."TestRenderChatAskUserShowsQuestionNotSpinnerasserts the focused question appears AND the thinking line is suppressed. Mirrors the default skin's focused questionnaire so the zeroline surface doesn't show a misleading "working…". -
Streaming text is kept plain on purpose.
renderAssistantroutes torenderAssistantPlainwhenmarkdown == false, and the comment names the reason: "live streaming text (markdown=false) stays plain because partial markdown renders badly mid-stream."TestStreamingAssistantStaysPlainasserts the streaming text appears verbatim. Correct — glamour would render half a**boldbadly. -
Streaming caret block is on the last streaming line.
s.block()returns a 1-cellBackground(Accent)" " and the transcript appends it to the last streaming line. Small but real UX detail that mirrors the default skin's streaming indicator. -
forceColor test helper is correctly scoped.
TestColorizeDiffColorsAddsAndDelscallsforceColor(t)to setlipgloss.SetColorProfile(termenv.TrueColor)for the test, witht.Cleanuprestoring the previous profile. A subsequent test that depends on no-color output won't be affected. Clean pattern. -
stripANSItest helper handles the common case. It strips ESC (0x1b) ...mANSI sequences. Doesn't handle cursor moves or other CSI, but the test assertions only check for colored text content, so it's sufficient. A future test that needs more could use a regex library. -
Diff detection is conservative.
looksLikeDifftriggers on@@,+++,---, or any+/-line in the body. A+just an additiontext would be misclassified as a diff, but theTestLooksLikeDifftable-driven test confirms the trade-off. The body-cap of 4 lines for grep, 8 for edits, 10 for bash means a misclassification only over-colorizes a few lines. -
Empty input is handled everywhere.
renderMarkdownreturns""forr == nil(the comment handles it via theif cached, ok := mdOutput[key]path with empty value fallback to plain).codeLinereturns""for empty input.permModalreturns empty for nilPerm(via the early return inRenderChat). No panics on empty data. -
Dependencies are called out explicitly. The body lists the transitive deps:
chroma,goldmark(+emoji),bluemonday,golang.org/x/net,golang.org/x/term, plus agolang.org/x/textbump.lipgloss/bubbletea/termenvwere already present.go mod tidywas run. The package is "pure presentation: callers build the data structs from live agent state; zeroline turns them into styled terminal frames" — no internal deps. -
The forward-code boundary is clean. No caller on
mainyet. The TUI model wires this in a later module; here it's exercised byrender_test.go/markdown_test.go/render_overlay_test.go. That's the right slicing for the runtime-core split.
Observations (non-blocking)
-
Naive cache reset on overflow (
mdCache = map[mdKey]...{}) drops ALL entries. A real LRU (e.g., a doubly-linked list keyed by recency) would preserve hot entries across overflow. The comment correctly says "simple bounded reset; cheap to repopulate" — and in practice the cache is bounded byvariants * 2 * widthswhich is small. The simpler reset is the right call here. -
mdOutCacheMax = 512andmdCacheMax = 256are package-level vars inmarkdown.go. If a future caller wanted a per-renderer budget (e.g., the TUI tightens it in a constrained mode), it would need a function parameter. The current global is fine; future refactor could pass them in. -
renderMarkdownis a package-private function with avarcache. A future caller outsidezerolinewould callRenderChatand get the cache. The internalmdMuis shared across callers, which is correct for a memoized renderer. -
The glamour version is pinned at v1.0.0 — a stable release. Bumping to a newer v1.x is a future concern. The transitive deps (chroma, goldmark, bluemonday) are all current majors.
-
newStylesandnewCanvasStylesdiffer only in the background of the foreground styles.newStylesbuildsf(c) = NewStyle().Foreground(c);newCanvasStylesbuildsf(c) = NewStyle().Foreground(c).Background(p.Bg). The two paths exist because text inside status bars (topBar/botBar) sits on aPanelbackground and should NOT bake in a different background of its own, while text on the home / boot splash sits on the full-bleedBgand must bake it in. The comment names the bug this guards: "otherwise the text shows the terminal's own background, producing a visible 'card' against the themed margins." Two functions is the right call. -
PermLayoutis exported andpermModalLinesis not. The TUI model needs the hitboxes for mouse handling; it doesn't need the raw modal lines. Correct API surface. -
permBoxWidthclamps the modal width to[38, 52]and tow-2. The 38-floor is a minimum readable width; the 52-cap is the design max. The w-2 clamp prevents the modal from exceeding the frame.TestPermLayoutMatchesRenderClampedexercises all three clamps. -
bar()fills the gap between left and right sections with a Panel-colored spacer. A simpler implementation could use lipglossWidth(w).Align(lipgloss.Right)but that doesn't render styled background. The current approach (build left, build right, build spacer, concat) is correct and explicit. -
The
wordmarkis a 5-line ASCII art constant. A future enhancement could load it from a file or accept a custom one. The current constant is fine for a fixed brand identity. -
spinFramesis a 10-element Braille spinner. A future enhancement could use a different spinner style per theme. The current set is sufficient. -
hasAssistantis a small helper that scansrowsfor the first assistant row. A future enhancement could memoize the last-seen run-state to avoid the scan on every frame. The current scan is O(n) where n is the visible transcript, which is bounded by the frame height. -
clipreturns""forw <= 0and the input verbatim if it already fits. The function is a thin wrapper aroundansi.Truncatefor the overflow case, with a wide-rune-correctness test.TestClipLeavesShortStringsAndZeroWidthcovers the boundary. -
padRightmeasureslipgloss.Widthof the input and pads towwith aBackground(fill)cell. The fill color is the parameter, which lets the same helper pad toPanel(status bar) orBg(transcript). A future enhancement could also expose a left-pad variant. -
wrapis a simple word-wrap that splits on whitespace and respects the width budget. It doesn't handle hyphenation, soft breaks, or long-word breaking. Good enough for prose. Code blocks bypasswrap(they're rendered withrenderCodeBlockwhich clips per line). -
shortPathcollapses paths longer than 3 segments to.../last/segment. A future enhancement could use a config-driven depth. The current 2-segment tail is a reasonable default. -
orDashreturns the input trimmed or an em-dash for empty. Used forBranch/Model/Providerto avoid showing a literal empty field. Small UX detail that matches what a coding agent wants to see. -
resultSummaryfor unknown tools clips the first 56 chars of the first line. A future enhancement could call into the tool's ownSummary()method. The current default is the safe fallback. -
TestPermLayoutMatchesRenderis the canonical hitbox test — it asserts the rendered button row contains both "allow" and "deny" labels, and a click in the middle of each button resolves correctly viag.Hit(x, y). A click at (0, 0) returns"". The hit-test is correct. -
TestRenderChatLiveDataexercises 4 transcript states — done (with assistant row), working+thinking (with spinner), streaming (with live text), permission (with modal). All four states are distinct visible behaviors. Comprehensive. -
The package doc comment is a one-paragraph mission statement that names the package's role ("renders the ZERO 'Zeroline' terminal surface"), the two surfaces ("Zen home page and a Statusline chat page"), the theming ("5 switchable color themes"), and the boundary ("pure presentation: callers build the data structs from live agent state; zeroline turns them into styled terminal frames"). A future reader can grep the doc to find the package's contract.
-
forceColoris a test helper that doesn't appear in production code. Thet.Cleanuprestore is correct — a test that doesn't restore would leak the color profile into the next test, producing color-dependent test order issues. -
No benchmark for
RenderChat(the hot path — every frame, every keystroke, every spinner tick). The cache and the clip helpers are the obvious targets. A benchmark suite would catch future regressions where someone adds an O(n) scan per frame. -
glamour.NewTermRendererreturns an error on init failure.markdownRendererreturnsnilon error, andrenderMarkdownfalls back to the raw text.TestRenderMarkdownStripsSyntaxexercises the happy path; the error path is not tested. A test could force a renderer failure (e.g., by passing an invalid style) to assert the fallback works. -
themes.godoesn't include theSel(selection) color in any visible spot. A future surface (e.g., the picker overlay) could use it as the selection color. CurrentlySelis defined but unreferenced from the render code — probably reserved for the future picker hitbox fill. -
themeStyleConfigoverrides most of the glamour default colors but does not override the heading underline color or the link underline color. A future enhancement could addcfg.Heading.Underlineandcfg.Link.Underlineto match the theme. The current behavior inherits glamour defaults which are usually fine. -
clipusesansi.Truncate(..., "…")— the ellipsis is a single character. For very narrow widths (e.g., 2 cells) the ellipsis is most of the output. A future enhancement could use a thinner ellipsis (...) for very narrow budgets. -
PermGeometry.Hitis inclusive on the left/top edges and exclusive on the right/bottom (x >= r.X && x < r.X + r.W && y >= r.Y && y < r.Y + r.H). Standard convention. Adjacent buttons don't overlap. A click exactly at the boundary resolves to the left/top button. The hitboxes are designed with gaps (Allow at x+2, width 13; Always at x+17, width 14; Deny at x+33, width 12) so a click on the gap returns"". -
colorizeDiff(the package-level helper) hardcodesvariant=0, dark=true. A future caller (e.g., a different theme) would need to pass them in. The current helper is for tests; the real renderer uses the per-callstyles.colorizeDiff(detail, tw)which uses the call's theme.
Approving — this is a substantial presentation layer with thorough test coverage. The mdCache and mdOutput are bounded (with tests proving the bound), keyed by variant+dark+width so theme changes and resizes get fresh renderers, and the non-reentrant mutex pattern is correct. The PermLayout/permModalLines lockstep guarantee is enforced at construction time (with a panic if the modal line count drifts) and tested at sub-floor sizes for both the width clamp and the height clamp. The overlay is correctly suppressed when a permission prompt is up, the frame height is invariant under overlay cap, and the clip helper is wide-rune-correct (CJK/emoji). The diff colorization uses the theme Red/Green (tested) and caps at 40 lines. The tool result collapsing is the right default for a coding agent transcript. The 5 themes are clean and small. The go.mod additions are explicit and called out in the body. The forward-code boundary is clean (no caller on main yet). The follow-up is the TUI model wiring, which lands in a later module.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughAdds a complete zeroline TUI renderer: five color themes, markdown->ANSI rendering with caching, RenderBoot/RenderHome/RenderChat surfaces, permission modal layout/hit-testing, overlay management, diff-aware tool output rendering, and extensive tests for clipping and overlays. ChangesZeroline TUI Rendering System
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with 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.
Inline comments:
In `@internal/zeroline/markdown_test.go`:
- Around line 177-186: The test TestStreamingAssistantStaysPlain currently only
checks for "partial" and can pass even if markdown markers are stripped; update
the assertion to verify the exact raw streaming fragment is preserved by
checking that stripANSI(RenderChat(d)) contains "partial **incomplete" (or both
"partial " and "**incomplete" together) so the markdown markers are not removed;
locate the test and the ChatData/RenderChat/stripANSI usage and replace the
current contains-check with an assertion that expects the full literal fragment
"partial **incomplete".
In `@internal/zeroline/render.go`:
- Around line 413-421: permBoxWidth can shrink too small causing the permission
modal's three-button row to overflow and leaving active hitboxes behind;
increase the minimum box width in permBoxWidth (replace the hard 38 minimum) to
at least the button-row requirement (e.g. 44) so the modal cannot be sized to
cause wrapping, and also guard the button-rendering/hitbox publishing in
permModalLines and the hitbox-publish section (the code around where hitboxes
are created/published) to skip rendering/publishing the button row when the
available width is less than the required button-row width. Ensure you reference
and update permBoxWidth and the conditional branches in permModalLines / the
hitbox publishing code so rendering and hitbox state remain consistent at narrow
widths.
- Around line 292-300: The overlay rendering currently only skips when d.Perm !=
nil or maxRows <= 0, but it must also suppress any picker/suggestions when
AskUser is active; update the early-return condition in the render block that
checks d.Perm and maxRows to also return when d.AskUser is true (so that
d.Picker, d.Suggestions, and d.SelectedIdx are not rendered while AskUser is
set), ensuring the check references d.AskUser alongside d.Perm and maxRows.
- Around line 1068-1092: The wrap function uses byte/char lengths
(len(cur)/len(word)) to decide line breaks, causing CJK/emoji width errors;
update the logic in wrap to compute widths with lipgloss.Width for cur and word
when comparing to w (e.g., replace len(...) checks with lipgloss.Width(...)),
and add unit tests exercising wrap directly (including CJK/emoji cases) similar
to existing TestClip* tests to verify display-cell-aware wrapping; ensure you
import github.com/charmbracelet/lipgloss and update any test names to cover the
plain assistant path for wrap.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 03e63cf0-1fb4-47b0-b38c-29692479ac8d
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (7)
go.modinternal/zeroline/markdown.gointernal/zeroline/markdown_test.gointernal/zeroline/render.gointernal/zeroline/render_overlay_test.gointernal/zeroline/render_test.gointernal/zeroline/theme.go
anandh8x
left a comment
There was a problem hiding this comment.
Revising my earlier APPROVE — CodeRabbit's CHANGES_REQUESTED review found 4 real issues I missed on my first pass. Walking through each:
1. TestStreamingAssistantStaysPlain has a weak assertion (real bug — test is silently weak)
Location: internal/zeroline/markdown_test.go:177-186
The test only asserts strings.Contains(out, "partial") on the rendered output. That passes even if the streaming fragment had its ** markdown markers stripped. The streaming path is supposed to keep the raw fragment verbatim (per the renderAssistantPlain contract), so the test should pin the exact fragment:
if !strings.Contains(out, "partial **incomplete") {
t.Errorf("streaming text must keep raw markdown markers verbatim, got: %q", out)
}A weak assertion is worse than no assertion — it gives false confidence. The whole point of renderAssistantPlain is "partial markdown renders badly mid-stream"; the test should prove the markers are kept.
2. permBoxWidth minimum of 38 is too small for the button row (real bug — buttons can render past the modal edge)
Location: internal/zeroline/render.go — permBoxWidth and the hitbox-publish section in permModalLines.
The button hitboxes are:
- Allow:
bx + 2, width 13 → ends atbx + 15 - Always:
bx + 17, width 14 → ends atbx + 31 - Deny:
bx + 33, width 12 → ends atbx + 45
The minimum modal width is 38, so bx = (width - 38) / 2. At bw = 38, the Deny button at bx + 33 extends to bx + 45, which is 7 cells past the right border of the modal. The button label and the hitbox would render outside the modal frame. TestPermLayoutMatchesRenderClamped runs at {38, 24} which exercises this exact path — the test passes because the "[ d · deny ]" label happens to start within the modal, but the right edge of the label and the hitbox W=12 are not actually verifiable inside the modal's clip.
Fix: raise the minimum to at least 45 (or the actual button-row width requirement) and guard the hitbox-publish so the buttons are inactive when the box is narrower than the button row. The lockstep guard in permModalLines (if len(lines) != permModalRows { panic(...) }) protects line count but not width. Add an if bw < minButtonRowWidth { return PermGeometry{Active: false} } guard in PermLayout mirroring the height-clamp guard at line if top+permBtnRow >= bodyH.
3. Overlay must also be suppressed when AskUser is active (real bug)
Location: internal/zeroline/render.go:292-300 — overlayRegion early-return condition.
Current code:
if d.Perm != nil || maxRows <= 0 {
return ""
}When d.AskUser != nil, the picker/suggestions overlay would still render between the command line and the bottom bar. The AskUser questionnaire already takes the place of the thinking/streaming line in the transcript, but a picker overlay above the cmd line would compete for screen space. Same lockstep concern as the permission prompt: the overlay regions are budgeted out of the body, so an active overlay during AskUser would shift the focused-question lines up/down.
Fix: add d.AskUser != nil to the early-return:
if d.Perm != nil || d.AskUser != nil || maxRows <= 0 {
return ""
}A test could exercise the new path: TestOverlaySuppressedDuringAskUser (mirror of TestPermLayoutMatchesRenderWithOverlayPresent) would assert that an active picker overlay is NOT rendered while AskUser is set.
4. wrap uses len() which is wrong for CJK/emoji (real bug — over-wraps wide runes)
Location: internal/zeroline/render.go — wrap(text string, w int) []string.
The function does Fields(text) and then len(cur)+1+len(word) <= w to decide joining. len() is byte length, not display width. CJK runes are 3 bytes UTF-8 but 2 display cells; emoji are 4 bytes UTF-8 but 2 display cells. So wrap over-wraps CJK text — the line that should fit is broken too early.
Example: with w=10, words ["世", "界", "世", "界"]:
- After "世" (len 3, width 2): cur len 3.
- "界" (len 3, width 2):
3+1+3=7 <= 10→ join. cur len 7, width 5. - "世" (len 3, width 2):
7+1+3=11 > 10→ break. cur len 7 stays. - But display width check:
5+1+2=8 <= 10→ should fit!
So wrap returns 3 lines where it should return 2, and the second line is shorter than necessary. Real layout bug for any CJK assistant output.
Fix: replace len(cur)+1+len(word) <= w with lipgloss.Width(cur)+1+lipgloss.Width(word) <= w. The package already imports lipgloss. Add unit tests on wrap directly (mirroring TestClipBudgetsByDisplayWidth): exercise wrap on CJK and emoji inputs and assert no emitted line exceeds w cells. A test like TestWrapBudgetsByDisplayWidth would catch this.
Note: clip already uses lipgloss.Width correctly (tested in TestClipBudgetsByDisplayWidth); wrap was missed. The two helpers should be consistent.
All four are real bugs that my first pass missed. The PR's overall design (bounded caches, lockstep modal, overlay cap, theme-aware diff colorization) is solid, but these four are blockers. Switching to CHANGES_REQUESTED pending fixes. Once addressed (with the wrap test in particular locking the new behavior), this is a clean approve.
Vasanthdev2004
left a comment
There was a problem hiding this comment.
Updating my verdict after the later review comments: request changes.
Validation is green (go test ./..., go run ./cmd/zero-release build, and go run ./cmd/zero-release smoke all pass locally), but I agree the current rendering layer still has a few TUI-contract blockers:
-
AskUsershould suppress overlays just like permissions do.RenderChattreatsAskUseras the blocking surface, butoverlayRegiononly skips onPerm, so stale suggestions/pickers can still render and consume body height while a focused question is active. Please add thed.AskUser != nilguard and coverAskUser + Suggestions/Pickerwith a regression test. -
The permission modal can overflow at narrow widths.
permBoxWidthfloors to 38, while the three-button row is wider than the modal content budget, andPermLayoutcan still publish active hitboxes. Please derive/enforce a minimum width from the rendered button row or suppress/publish hitboxes consistently when the buttons cannot fit, then add a small-width line-width/hitbox test. -
Plain assistant wrapping still uses byte length and does not hard-clip long words/tokens. Markdown output is clipped, but streaming/plain output can exceed the frame for CJK/emoji or a long URL-like token. Please switch wrap decisions to display width and make the plain path guarantee every rendered line stays within budget.
The streaming verbatim test comment is a good minor cleanup too: assert the literal partial **incomplete fragment so the test proves markdown is not applied while streaming.
…ender-contract fixes - AskUser suppresses overlays like Perm does: overlayRegion now returns early on d.AskUser != nil so suggestions/picker rows can't consume bodyH and push the focused questionnaire offscreen. Test: TestOverlaySuppressedDuringAskUser. - Permission modal narrow-width: derived permMinBoxWidth=47 (43-cell button row + border/padding); PermLayout disables the hitboxes when the box is narrower (buttons would overflow/clip and stop matching), mirroring the vertical-clip guard. Rewrote TestPermLayoutMatchesRenderClamped: narrow widths -> inactive, wide+height-clamped -> lockstep. - wrap() now budgets by display width (lipgloss.Width) instead of byte length, so CJK/emoji don't break the width budget. Test: TestWrapBudgetsByDisplayWidth. - Plain/streaming path now clips each wrapped prose line: wrap doesn't break a single over-long token (long URL / unbroken CJK), so renderAssistantPlain clips wl to the budget. Test: TestPlainAssistantClipsLongUnbrokenToken. - Streaming verbatim test strengthened to assert the literal 'partial **incomplete' survives (proves glamour is not applied while streaming). build/vet/-race/full-suite + GOOS=windows build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed all review feedback (@coderabbitai @anandh8x @Vasanthdev2004) — all 4 findings plus the extra long-token case:
build/vet/-race/full-suite + |
|
Kicking off a fresh review now. 🧠 Learnings used✅ Action performedReview finished.
|
There was a problem hiding this comment.
🧹 Nitpick comments (3)
internal/zeroline/render_overlay_test.go (1)
37-41: 💤 Low valueConsider verifying picker suppression as well.
The test data includes both
SuggestionsandPicker, but only verifies that suggestion content is suppressed. For completeness, you could also assert that the picker items ("a","b","c") or title ("pick") don't appear.🤖 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/zeroline/render_overlay_test.go` around lines 37 - 41, The test currently checks that suggestion text is suppressed by inspecting full := stripANSI(out) and asserting absence of "show help" and "switch model"; extend this to also assert the picker content is suppressed by verifying that the picker title "pick" and the picker items "a", "b", and "c" do not appear in full (use strings.Contains on full as with the existing checks and t.Errorf on failure) so both Suggestions and Picker outputs are validated as suppressed.internal/zeroline/markdown_test.go (1)
79-81: ⚖️ Poor tradeoffConsider making mdCache injectable for testing.
The test directly manipulates the global
mdCacheandmdMuto verify cache bounding. While this works (Go tests in the same package run sequentially by default), it creates coupling to internal implementation details and would break if tests were parallelized.Consider adding a
resetMarkdownCache()test helper or making the cache injectable through a package-level function. However, given that this test suite doesn't uset.Parallel()and the current approach works reliably, this is purely a nice-to-have.🤖 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/zeroline/markdown_test.go` around lines 79 - 81, The test mutates package globals mdCache and mdMu which couples tests to implementation; add a small package-level test helper such as resetMarkdownCache() (or SetMarkdownCache(map[mdKey]*glamour.TermRenderer) to inject a cache) and use that in tests instead of directly locking and assigning mdCache/mdMu; implement resetMarkdownCache() to lock mdMu, replace mdCache with an empty map, and unlock, or provide an injectable constructor (e.g., NewMarkdownRendererWithCache) so tests can supply an isolated cache.internal/zeroline/render_test.go (1)
271-286: 💤 Low valuestripANSI only handles SGR sequences.
The helper correctly strips color codes (SGR sequences ending in 'm') from lipgloss output, which is its intended purpose. However, it would get stuck in escape mode if the input contained other CSI sequences like cursor movement (ending in 'H', 'J', etc.). This is fine for current test usage but worth noting as a limitation.
More robust ANSI stripper (if needed beyond lipgloss output)
func stripANSI(s string) string { var b strings.Builder inEsc := false for _, r := range s { switch { case r == 0x1b: inEsc = true - case inEsc && (r == 'm'): + case inEsc && (r >= '@' && r <= '~'): + // CSI sequences end with a byte in range 0x40-0x7E inEsc = false case inEsc: default: b.WriteRune(r) } } return b.String() }🤖 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/zeroline/render_test.go` around lines 271 - 286, stripANSI currently only stops escape mode on SGR ('m') sequences and will hang if other CSI sequences appear; update the stripANSI function to detect CSI sequences (escape followed by '[') and consume runes until the CSI final byte (a rune in the range '@'..'~') instead of only checking for 'm', so inEsc is cleared for any CSI sequence; keep the existing handling for plain ESC and SGR but broaden the switch in stripANSI to recognize the '[' after 0x1b and skip until the CSI terminator to avoid getting stuck on cursor/movement sequences.
🤖 Prompt for all review comments with 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.
Nitpick comments:
In `@internal/zeroline/markdown_test.go`:
- Around line 79-81: The test mutates package globals mdCache and mdMu which
couples tests to implementation; add a small package-level test helper such as
resetMarkdownCache() (or SetMarkdownCache(map[mdKey]*glamour.TermRenderer) to
inject a cache) and use that in tests instead of directly locking and assigning
mdCache/mdMu; implement resetMarkdownCache() to lock mdMu, replace mdCache with
an empty map, and unlock, or provide an injectable constructor (e.g.,
NewMarkdownRendererWithCache) so tests can supply an isolated cache.
In `@internal/zeroline/render_overlay_test.go`:
- Around line 37-41: The test currently checks that suggestion text is
suppressed by inspecting full := stripANSI(out) and asserting absence of "show
help" and "switch model"; extend this to also assert the picker content is
suppressed by verifying that the picker title "pick" and the picker items "a",
"b", and "c" do not appear in full (use strings.Contains on full as with the
existing checks and t.Errorf on failure) so both Suggestions and Picker outputs
are validated as suppressed.
In `@internal/zeroline/render_test.go`:
- Around line 271-286: stripANSI currently only stops escape mode on SGR ('m')
sequences and will hang if other CSI sequences appear; update the stripANSI
function to detect CSI sequences (escape followed by '[') and consume runes
until the CSI final byte (a rune in the range '@'..'~') instead of only checking
for 'm', so inEsc is cleared for any CSI sequence; keep the existing handling
for plain ESC and SGR but broaden the switch in stripANSI to recognize the '['
after 0x1b and skip until the CSI terminator to avoid getting stuck on
cursor/movement sequences.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: bf70eb43-dbe5-463e-9cfc-ac51de7fab9a
📒 Files selected for processing (4)
internal/zeroline/markdown_test.gointernal/zeroline/render.gointernal/zeroline/render_overlay_test.gointernal/zeroline/render_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/zeroline/render.go
anandh8x
left a comment
There was a problem hiding this comment.
What's good
All four issues from my earlier CHANGES_REQUESTED are addressed, plus a 5th fix that the author caught while resolving them. Walking through each:
-
#1
TestStreamingAssistantStaysPlainstrengthened. The assertion now checksstrings.Contains(out, "partial **incomplete")— pins the literal fragment, so a regression that strips the**markers (i.e., a future refactor that accidentally routes streaming through glamour) would fail. The error message names the threat: "streaming text not verbatim (markdown markers stripped?)". A weak assertion is worse than no assertion; this is now a real test. -
#2
permMinBoxWidth = 47constant with derived rationale. The comment is explicit: "43 button-row cells ([ a · allow ]13 + 2 gap +[ y · always ]14 + 2 gap +[ d · deny ]12) plus a 1-cell border and 1-cell padding on each side". My earlier math said 44/45; the author derived 47 from the actual button strings inpermModalLines(the always/deny labels are 14/12 wide including the bracket pair, not the visible-character count I used). The new guard inPermLayout:if bw < permMinBoxWidth { return PermGeometry{Active: false} }
Mirrors the existing vertical-clip guard and is placed in lockstep with it. The comment names the bug this guards: "the button row would overflow the box border and can clip at the terminal edge, so the rendered buttons no longer reliably match these hitboxes. Disable them (the keyboard shortcuts still work) — mirrors the vertical-clip guard below." A future vertically-stacked narrow layout is correctly flagged as "a future enhancement".
-
#3
overlayRegionnow suppresses onAskUsertoo. The early-return is nowd.Perm != nil || d.AskUser != nil || maxRows <= 0. The doc comment above the function is updated to name both cases: "For an ask_user questionnaire, RenderChat treats it as the focused blocking surface, so overlay rows would consume bodyH and push the question offscreen."TestOverlaySuppressedDuringAskUser(new) feeds anAskUserplus an activeSuggestionslist with a/zzsuggesttoken and asserts the suggestion is NOT in the rendered output. A future overlay type that needs to render duringAskUser(unlikely) would have to be added explicitly. -
#4
wrapnow useslipgloss.Width. The switch case changed fromlen(cur)+1+len(word) <= wtolipgloss.Width(cur)+1+lipgloss.Width(word) <= w.TestWrapBudgetsByDisplayWidth(new) exercises"你好 世界"atw=10and asserts the function returns one line, not two — proving the function uses display width (4+1+4=9 ≤ 10) instead of byte length (6+1+6=13, which would force a wrap). The comment in the test is a model: "wrap must use display width like clip does". The fix is consistent withclip,padRight, and the other width-aware helpers. -
#5 (bonus)
renderAssistantPlainclips the wrapped line too. A long unbroken token (URL or unbroken CJK run) is not broken bywrap(it splits on whitespace), so the new lineout = append(out, " "+s.fg.Render(clip(wl, tw-9)))appliesclipper wrapped line. The comment is clear: "clip the wrapped line too: wrap splits on spaces and does not break a single word longer than the budget, so a long URL-like token or an unbroken CJK run would otherwise overflow the frame."TestPlainAssistantClipsLongUnbrokenToken(new) feeds a 400-char"x"repeat through streaming, scans for the rendered token, and asserts no line containing it exceeds the frame width. Without this fix, the previousTestAssistantMarkdownClipsLongLines(glamour path) was fine but the plain path could still overflow.
Test quality of the new tests
TestWrapBudgetsByDisplayWidthusesstrings.Repeatand a single line of assertion: a focused regression test that would catch a future revert to byte-length checks.TestOverlaySuppressedDuringAskUseruses a/zzsuggesttoken (deliberately obscure) so a future test run can't accidentally match a real command. The token is asserted absent, not present — the right polarity for a suppression test.TestPlainAssistantClipsLongUnbrokenTokeniterates the rendered output and checks every line containing the token. A future regression that adds a single overflow line would fail. The test scope comment is precise: "Scoped to the prose line carrying the token (the statusline footer width is a separate concern)." That's a thoughtful scope-bound.TestPermLayoutMatchesRenderClampedwas rewritten cleanly: it now has two explicit blocks — "too narrow → inactive" and "wide enough → lockstep" — with the lockstep block unchanged. The inactive-block comment names the new guard: "the buttons would overflow the modal frame / clip at the terminal edge, so hitboxes are disabled (keyboard still works)". The sizes{10,24}, {40,24}, {48,24}all yieldbw < 47afterpermBoxWidthclamping (38/38/46 respectively), so all three must be inactive. Good coverage of the new branch.
Other observations (non-blocking)
-
permMinBoxWidth = 47is a package-private const next topermModalRowsandpermBtnRow. The trio of constants now lives together and the new comment derives 47 from the actual button strings. If the button labels change in the future, the constant needs a re-derivation. A test that asserts the constant matches the actual button width (a structural assertion) would catch this, but a unit test for "43 + 4 = 47" is overkill. The current code is fine. -
permBoxWidthstill floors to 38 for non-permission widths (e.g., when called fromRenderHome's input box). The 38 minimum there is a different concern (readability of the home input). Keeping the two floors separate is correct. -
The test
TestPermLayoutMatchesRenderClampedfor the inactive case is short (3 sizes, all assert!g.Active). A future enhancement could also assert the frame height stays correct when hitboxes are inactive —RenderChatshould still render a usable modal that looks the same, just with the hitboxes disabled. The current test scope is fine. -
TestPlainAssistantClipsLongUnbrokenTokenonly checks the line containing the token (if strings.Contains(ln, "xxx")). A future regression that adds a second line that overflows (e.g., from a wrap call elsewhere) would not be caught. The test's scope is the streaming path, so this is OK. -
TestWrapBudgetsByDisplayWidthis a single-case test. The original CodeRabbit feedback suggested "a test exercising wrap directly (including CJK/emoji cases) similar to existing TestClip* tests". A more comprehensive test (e.g., mixing CJK and ASCII, testing wrap at narrow widths, testing wrap with emoji) would be a nice future enhancement. The current test pins the bug fix; additional cases are nice-to-have. -
The
wrapchange ripples intorenderAssistantPlainbut the test is inrender_overlay_test.go(named for the overlay tests but the file groups other tests too). A future refactor could move the wrap test into its ownwrap_test.go. Minor. -
clipandwrapare now consistent — both uselipgloss.Widthand both have a regression test pinning the behavior. Good consistency. Future helpers in this file should follow the same pattern. -
The doc comment for
wrapcould be updated to mention that it uses display width (matching theclipdoc comment's explicit mention of display width vs rune count). A one-line addition would help future maintainers. -
CI is green on all platforms. Build/vet/-race/full-suite +
GOOS=windowsbuild all pass per the commit message. CodeRabbit status check shows SUCCESS. No regression risk visible. -
The commit message is well-structured — it lists the 5 fixes in order, names the test added for each, and confirms green CI. A future bisect on this PR can read the commit message and know exactly what changed and why.
Approving — the four issues from my first pass are all addressed with focused fixes and regression tests, and the author caught a 5th issue (long unbroken token overflow in the plain path) while resolving them. The lockstep guarantee between PermLayout and the rendered modal is now enforced on BOTH axes (height-clamp and width-clamp), the overlay suppression is correct on both Perm and AskUser, and the wrap/clip/renderAssistantPlain helpers are all display-width-aware. The new tests pin the new behavior so a future regression would fail. CI is green. Clean approve.
Vasanthdev2004
left a comment
There was a problem hiding this comment.
Re-review on 014fa2e: most of the previous blockers are fixed, but I still need to request changes for one remaining render-contract issue.
The AskUser overlay suppression, display-width wrapping, plain-path clipping, and streaming-verbatim test are fixed and look good. Local validation is green:
go test ./...go run ./cmd/zero-release buildgo run ./cmd/zero-release smoke
Remaining blocker:
internal/zeroline/render.go:632-643 still renders the full three-button permission row even when PermLayout disables hitboxes for narrow widths. For example, at terminal width 40, permBoxWidth returns 38, but buttons is 43 cells and content(buttons) adds borders/padding without clipping or replacing it. That makes the rendered row wider than the frame even though the hitboxes are inactive. The fix should keep render + hit-test behavior aligned: when bw < permMinBoxWidth, render a compact keyboard-only hint/blank row instead of the full buttons, or otherwise guarantee every modal line fits the frame. Please add a regression test that renders RenderChat(... Width: 40, Perm: ...) and asserts every line stays within the frame width while PermLayout(...).Active == false.
…l chrome within the frame - permModalLines: when bw < permMinBoxWidth (too narrow for the 43-cell button row) it no longer renders the full button row (which overflowed the modal frame); it renders a compact keyboard-only hint clipped to the content width. Render and hit-test now stay aligned: PermLayout already disables the mouse hitboxes at this width. - RenderChat: added clampFrameWidth — a final frame-safety pass that clips every composed line to the frame width, so chrome (top/bottom bars) and the modal/body can never overflow the terminal at narrow widths (fixes the header/statusline-footer overflow too). Width-aware + ANSI-safe; a no-op at normal widths. - Regression test TestPermModalFitsFrameAtNarrowWidth: RenderChat at Width:40 with a Perm prompt asserts every line stays within 40 cells while PermLayout(40,24).Active == false. build/vet/-race/full-suite + GOOS=windows build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@Vasanthdev2004 fixed the remaining blocker:
build/vet/-race/full-suite + |
Vasanthdev2004
left a comment
There was a problem hiding this comment.
Approved on 4c886a3.
The narrow permission modal blocker is fixed: permModalLines now swaps the overflowing button row for a compact keyboard hint when bw < permMinBoxWidth, PermLayout keeps hitboxes inactive for that same narrow state, and TestPermModalFitsFrameAtNarrowWidth covers Width: 40 by asserting every rendered line stays within the frame.
I also rechecked the earlier fixes and they still look good: AskUser suppresses overlays, plain wrapping/clipping is display-width safe, and streaming preserves raw markdown markers.
Local validation passed:
go test ./internal/zeroline -run TestPermModalFitsFrameAtNarrowWidth -count=1go test ./...go run ./cmd/zero-release buildgo run ./cmd/zero-release smoke
GitHub Actions are green. CodeRabbit is still finishing its latest run at the moment of this review, but I don’t see any remaining human-review blockers.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with 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.
Inline comments:
In `@internal/zeroline/render.go`:
- Around line 652-658: When rendering the narrow fallback (bw < permMinBoxWidth)
remove the mouse/click hint and only include the keyboard hints by gating any
"click a choice" prompt on PermLayout(w, d.Height).Active; update the code path
that builds buttons (the clip(...) call using s.acc.Render and s.dim.Render) and
ensure cmdRegion only emits a click prompt when PermLayout(...).Active is true.
Also add/adjust the unit TestPermModalFitsFrameAtNarrowWidth to assert
PermLayout(width, d.Height).Active == false for the tested narrow width and
verify the command hint does not advertise clicking (i.e., no "click" or
mouse-choice text) so the test locks the expected narrow behavior.
🪄 Autofix (Beta)
✅ Autofix completed
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 4fad6d1e-9f2a-4930-89c3-298d4d665cf2
📒 Files selected for processing (2)
internal/zeroline/render.gointernal/zeroline/render_test.go
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 2 file(s) based on 1 unresolved review comment. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 2 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
|
@coderabbitai review — gated the command hint on |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Fetch relevant sections of render.go — permModal, permModalLines, clampFrameWidth, RenderChat hint logic
rg -n "PermLayout|permMinBoxWidth|clampFrameWidth|keyboard|click|keys a|hint|Active" internal/zeroline/render.go | head -80
echo "---permModalLines---"
ast-grep --pattern 'func permModalLines($$$) $_ {
$$$
}' internal/zeroline/render.go
echo "---RenderChat hint block---"
rg -n -A5 -B2 "click a choice\|keys a/y" internal/zeroline/render.go
echo "---TestPermModalFitsFrameAtNarrowWidth---"
rg -n -A40 "TestPermModalFitsFrameAtNarrowWidth" internal/zeroline/render_test.goLength of output: 3524
What I verified:
One trivial observation: No outstanding issues — this is good to merge. [approve] 🧠 Learnings used |
Module of N — TUI rendering (
zeroline)Reviewable slice of the runtime-core split (off
main). Scope:internal/zerolineonly (6 files, ~1,988 lines incl. tests). Pure presentation layer — no internal deps; a clean new package (no drift).What's in it
mdCacheso we don't re-render every frame, plus chroma syntax highlighting (transitive via glamour).theme.go).zerolineturns them into styled terminal frames.Forward code
No caller on
mainyet — the TUI model wires this in a later module; here it's exercised byrender_test.go/markdown_test.go/render_overlay_test.go.Dependencies (called out)
Adds
github.com/charmbracelet/glamour v1.0.0, which pullsalecthomas/chroma/v2,yuin/goldmark(+emoji),microcosm-cc/bluemonday,golang.org/x/net,golang.org/x/term(and bumpsgolang.org/x/text) transitively.lipgloss/bubbletea/termenvwere already present. Resolved viago mod tidy.Testing
go build ./...,go vet ./...,go test ./...,go test -race ./internal/zeroline/all green; no behavior change to existing packages.Part of decomposing #101 (draft); subagents excluded.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests