Problem
The multi-line status panel in TerminalRepl renders with garbled layout — lines visually overflow the terminal width, producing stray … artifacts at the right edge, and sometimes collapsing multiple logical lines into a single visual row.
Observed output (messy):
◆ claude-opus-4-6 | branch=codex/issue-215-learning-... | 14/200K (0%) | 17:05:32 …
├─ 🧠 learning | replay=measured:p=0.00,d=0.00,fp=0.00,r=0.00 | candidates=29(p:5,s:24,d:0) | ... …
├─ 🔄 cron | scheduler=on | state=idle | jobs=3 (s:2,h:1,o:0) …
│ ▸ acn-earnings-alert [scheduled] enabled next=149h 54m @ 23:00:00 :: Search for Accenture ... …
│ ▸ hb-accenture-global-news [heartbeat-longterm] enabled next=08:10:00 :: 搜索 Accenture 最新全... …
├─ 🚀 tasks | running=0 | permissions=0 …
🔔 notices …
Lines should be cleanly truncated to terminal width without stray artifacts or wrapping.
Root Cause Analysis
Three interacting bugs in the status line rendering pipeline:
1. Width calculation mismatch: displayWidth() vs JLine columnLength()
TerminalTheme.displayWidth() (line 202) uses a custom isWideChar() table to compute column widths. clampAttributedLine() (TerminalRepl line 2403) uses JLine's AttributedString.columnLength() and columnSubSequence(). These two implementations can disagree on specific code points (emoji sequences, box-drawing characters, CJK), causing the padding math to be wrong — lines are padded to a width JLine thinks is correct but the terminal does not agree with.
2. Hardcoded safeWidth = terminalWidth - 12 is too crude
renderStatusFrame() line 1287:
int safeWidth = Math.max(20, terminalWidth - 12);
The -12 margin is a static guess. But different status lines have wildly different indentation overhead:
- Header line:
"◆ " — 3 display columns of prefix
- Section headers:
" ├─ 🔄 " — ~9 display columns
- Cron job details:
" │ ▸ " — ~10 display columns, plus variable-length job.id(), [kind], enabled, next=... before the truncated description even starts
A cron detail line can easily consume 80+ columns of structured content before reaching the fitWidth(description, 24) portion, leaving zero room for the line to fit in safeWidth.
3. Double truncation stacks ... ellipses
Two independent truncation mechanisms both append ...:
- Content-level:
fitWidth(job.description(), 24) in buildStatusPanelLines() (line 1432) — truncates the description field and appends ...
- Display-level:
clampAttributedLine() (line 2417) — truncates the entire line and appends ...
When both fire, you get "Search for Accenture ..." from fitWidth, then clampAttributedLine truncates further and adds its own ..., producing redundant ellipses.
Files Involved
aceclaw-cli/src/main/java/dev/aceclaw/cli/TerminalRepl.java — renderStatusFrame(), buildStatusPanelLines(), clampAttributedLine()
aceclaw-cli/src/main/java/dev/aceclaw/cli/TerminalTheme.java — fitWidth(), displayWidth(), isWideChar()
Suggested Fix
-
Unify width calculation: Use a single source of truth for display width. Either delegate entirely to JLine's WCWidth / columnLength(), or use displayWidth() everywhere and avoid JLine's column methods for truncation. Add unit tests asserting both agree on emoji, CJK, and box-drawing chars.
-
Make safeWidth per-line or compute available content width dynamically: Instead of a global -12, compute the actual prefix width consumed by indentation/tree-chars/icons for each line, and truncate content to terminalWidth - prefixWidth (with a small safety margin).
-
Single-pass truncation: Remove the content-level fitWidth() calls inside buildStatusPanelLines() — let clampAttributedLine() be the sole truncation point. Or, if content-level truncation is preferred for readability, skip the display-level truncation for lines that have already been fitted.
-
Replace space-padding with ANSI erase-to-end-of-line: Instead of " ".repeat(clearWidth - visibleWidth), emit \033[K (erase to EOL) after each status line. This avoids width-dependent padding entirely and is more robust against miscalculation.
Problem
The multi-line status panel in
TerminalReplrenders with garbled layout — lines visually overflow the terminal width, producing stray…artifacts at the right edge, and sometimes collapsing multiple logical lines into a single visual row.Observed output (messy):
Lines should be cleanly truncated to terminal width without stray artifacts or wrapping.
Root Cause Analysis
Three interacting bugs in the status line rendering pipeline:
1. Width calculation mismatch:
displayWidth()vs JLinecolumnLength()TerminalTheme.displayWidth()(line 202) uses a customisWideChar()table to compute column widths.clampAttributedLine()(TerminalRepl line 2403) uses JLine'sAttributedString.columnLength()andcolumnSubSequence(). These two implementations can disagree on specific code points (emoji sequences, box-drawing characters, CJK), causing the padding math to be wrong — lines are padded to a width JLine thinks is correct but the terminal does not agree with.2. Hardcoded
safeWidth = terminalWidth - 12is too cruderenderStatusFrame()line 1287:The
-12margin is a static guess. But different status lines have wildly different indentation overhead:"◆ "— 3 display columns of prefix" ├─ 🔄 "— ~9 display columns" │ ▸ "— ~10 display columns, plus variable-lengthjob.id(),[kind],enabled,next=...before the truncated description even startsA cron detail line can easily consume 80+ columns of structured content before reaching the
fitWidth(description, 24)portion, leaving zero room for the line to fit insafeWidth.3. Double truncation stacks
...ellipsesTwo independent truncation mechanisms both append
...:fitWidth(job.description(), 24)inbuildStatusPanelLines()(line 1432) — truncates the description field and appends...clampAttributedLine()(line 2417) — truncates the entire line and appends...When both fire, you get
"Search for Accenture ..."fromfitWidth, thenclampAttributedLinetruncates further and adds its own..., producing redundant ellipses.Files Involved
aceclaw-cli/src/main/java/dev/aceclaw/cli/TerminalRepl.java—renderStatusFrame(),buildStatusPanelLines(),clampAttributedLine()aceclaw-cli/src/main/java/dev/aceclaw/cli/TerminalTheme.java—fitWidth(),displayWidth(),isWideChar()Suggested Fix
Unify width calculation: Use a single source of truth for display width. Either delegate entirely to JLine's
WCWidth/columnLength(), or usedisplayWidth()everywhere and avoid JLine's column methods for truncation. Add unit tests asserting both agree on emoji, CJK, and box-drawing chars.Make
safeWidthper-line or compute available content width dynamically: Instead of a global-12, compute the actual prefix width consumed by indentation/tree-chars/icons for each line, and truncate content toterminalWidth - prefixWidth(with a small safety margin).Single-pass truncation: Remove the content-level
fitWidth()calls insidebuildStatusPanelLines()— letclampAttributedLine()be the sole truncation point. Or, if content-level truncation is preferred for readability, skip the display-level truncation for lines that have already been fitted.Replace space-padding with ANSI erase-to-end-of-line: Instead of
" ".repeat(clearWidth - visibleWidth), emit\033[K(erase to EOL) after each status line. This avoids width-dependent padding entirely and is more robust against miscalculation.