feat(tui): segment turns with rule above non-first user msgs; trim ticker dead space#21846
Conversation
…cker dead space Multi-turn transcripts ran together visually because every user message got the same vertical rhythm regardless of position. Adds a short ─── in the border colour above every user message after the first, so each turn reads as its own block. Height estimator gains a `withSeparator` flag so virtual scrolling pre-allocates the extra two rows (rule + top margin) and avoids a jump on first measurement. While in the area: the busy-indicator duration was padded with `padStart(7)`, leaving five visible spaces between `·` and the digits (`⠋ · 2s`) — especially loud under the verb-less `unicode` style. Drop the padding entirely (`⠋ · 2s`); the model label now shifts a few columns as the duration grows, which is the right trade-off for the minimal indicator styles. The verb-padding test stays; the duration-padding test is removed alongside the function it covered.
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-attribute |
25 |
invalid-assignment |
6 |
First entries
run_agent.py:9575: [invalid-assignment] invalid-assignment: Object of type `int` is not assignable to attribute `last_prompt_tokens` on type `None | Unknown | ContextCompressor`
tests/run_agent/test_compressor_fallback_update.py:70: [unresolved-attribute] unresolved-attribute: Attribute `provider` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:12096: [invalid-assignment] invalid-assignment: Object of type `Literal[False]` is not assignable to attribute `_context_probe_persistable` on type `None | Unknown | ContextCompressor`
tests/run_agent/test_switch_model_context.py:49: [unresolved-attribute] unresolved-attribute: Attribute `context_length` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:9453: [unresolved-attribute] unresolved-attribute: Attribute `compress` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:13803: [unresolved-attribute] unresolved-attribute: Attribute `should_compress` is not defined on `None` in union `None | Unknown | ContextCompressor`
gateway/run.py:9860: [unresolved-attribute] unresolved-attribute: Attribute `has_content_to_compress` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:12994: [unresolved-attribute] unresolved-attribute: Attribute `update_model` is not defined on `None` in union `None | Unknown | ContextCompressor`
tests/run_agent/test_compression_feasibility.py:346: [unresolved-attribute] unresolved-attribute: Attribute `threshold_tokens` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:10394: [unresolved-attribute] unresolved-attribute: Attribute `handle_tool_call` is not defined on `None` in union `None | Unknown | ContextCompressor`
tests/run_agent/test_compressor_fallback_update.py:72: [unresolved-attribute] unresolved-attribute: Attribute `threshold_tokens` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:12085: [unresolved-attribute] unresolved-attribute: Attribute `update_from_response` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:11013: [unresolved-attribute] unresolved-attribute: Attribute `protect_first_n` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:13807: [unresolved-attribute] unresolved-attribute: Attribute `last_prompt_tokens` is not defined on `None` in union `None | Unknown | ContextCompressor`
tests/run_agent/test_compressor_fallback_update.py:67: [unresolved-attribute] unresolved-attribute: Attribute `model` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:2778: [invalid-assignment] invalid-assignment: Object of type `int | float` is not assignable to attribute `threshold_percent` on type `None | Unknown | ContextCompressor`
run_agent.py:9556: [unresolved-attribute] unresolved-attribute: Attribute `compression_count` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:12917: [unresolved-attribute] unresolved-attribute: Attribute `context_length` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:2772: [invalid-assignment] invalid-assignment: Object of type `int` is not assignable to attribute `threshold_tokens` on type `None | Unknown | ContextCompressor`
tests/run_agent/test_compressor_fallback_update.py:68: [unresolved-attribute] unresolved-attribute: Attribute `base_url` is not defined on `None` in union `None | Unknown | ContextCompressor`
tests/run_agent/test_switch_model_context.py:60: [unresolved-attribute] unresolved-attribute: Attribute `model` is not defined on `None` in union `None | Unknown | ContextCompressor`
cli.py:8006: [unresolved-attribute] unresolved-attribute: Attribute `last_prompt_tokens` is not defined on `None` in union `None | Unknown | ContextCompressor`
tests/run_agent/test_compressor_fallback_update.py:72: [unresolved-attribute] unresolved-attribute: Attribute `threshold_percent` is not defined on `None` in union `None | Unknown | ContextCompressor`
cli.py:8009: [unresolved-attribute] unresolved-attribute: Attribute `compression_count` is not defined on `None` in union `None | Unknown | ContextCompressor`
cli.py:8007: [unresolved-attribute] unresolved-attribute: Attribute `context_length` is not defined on `None` in union `None | Unknown | ContextCompressor`
... and 6 more
✅ Fixed issues: none
Unchanged: 4034 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
There was a problem hiding this comment.
Pull request overview
This PR polishes the --tui transcript and status bar rendering by (1) visually separating multi-turn user messages with a small rule above non-first user turns, and (2) removing unnecessary left-padding in the status ticker’s elapsed-duration segment to avoid visible “dead space,” especially in minimal indicator styles.
Changes:
- Add an inter-turn separator rule above every user message after the first, and ensure virtualized row measurement includes its vertical space.
- Extend
estimatedMsgHeightwith an optionalwithSeparatorflag so initial height estimates account for the separator before real measurements arrive. - Remove duration padding in the status ticker and delete the now-stale duration-padding test.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| ui-tui/src/lib/virtualHeights.ts | Adds withSeparator to height estimation so virtualized rows reserve space for the new inter-turn rule. |
| ui-tui/src/components/appLayout.tsx | Renders a short ─── rule (with top margin) above non-first user messages inside the measured row wrapper. |
| ui-tui/src/components/appChrome.tsx | Removes duration left-padding so ticker durations don’t introduce leading gaps in minimal indicator styles. |
| ui-tui/src/app/useMainApp.ts | Computes firstUserIdx and passes withSeparator to estimatedMsgHeight using the same predicate as the renderer. |
| ui-tui/src/tests/virtualHeights.test.ts | Adds a test asserting withSeparator increases the estimate by exactly 2 rows. |
| ui-tui/src/tests/statusBarTicker.test.ts | Removes the duration-padding test and associated imports after padding removal. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…cker dead space (NousResearch#21846) Multi-turn transcripts ran together visually because every user message got the same vertical rhythm regardless of position. Adds a short ─── in the border colour above every user message after the first, so each turn reads as its own block. Height estimator gains a `withSeparator` flag so virtual scrolling pre-allocates the extra two rows (rule + top margin) and avoids a jump on first measurement. While in the area: the busy-indicator duration was padded with `padStart(7)`, leaving five visible spaces between `·` and the digits (`⠋ · 2s`) — especially loud under the verb-less `unicode` style. Drop the padding entirely (`⠋ · 2s`); the model label now shifts a few columns as the duration grows, which is the right trade-off for the minimal indicator styles. The verb-padding test stays; the duration-padding test is removed alongside the function it covered.
…cker dead space (NousResearch#21846) Multi-turn transcripts ran together visually because every user message got the same vertical rhythm regardless of position. Adds a short ─── in the border colour above every user message after the first, so each turn reads as its own block. Height estimator gains a `withSeparator` flag so virtual scrolling pre-allocates the extra two rows (rule + top margin) and avoids a jump on first measurement. While in the area: the busy-indicator duration was padded with `padStart(7)`, leaving five visible spaces between `·` and the digits (`⠋ · 2s`) — especially loud under the verb-less `unicode` style. Drop the padding entirely (`⠋ · 2s`); the model label now shifts a few columns as the duration grows, which is the right trade-off for the minimal indicator styles. The verb-padding test stays; the duration-padding test is removed alongside the function it covered.
…cker dead space (NousResearch#21846) Multi-turn transcripts ran together visually because every user message got the same vertical rhythm regardless of position. Adds a short ─── in the border colour above every user message after the first, so each turn reads as its own block. Height estimator gains a `withSeparator` flag so virtual scrolling pre-allocates the extra two rows (rule + top margin) and avoids a jump on first measurement. While in the area: the busy-indicator duration was padded with `padStart(7)`, leaving five visible spaces between `·` and the digits (`⠋ · 2s`) — especially loud under the verb-less `unicode` style. Drop the padding entirely (`⠋ · 2s`); the model label now shifts a few columns as the duration grows, which is the right trade-off for the minimal indicator styles. The verb-padding test stays; the duration-padding test is removed alongside the function it covered.
…cker dead space (NousResearch#21846) Multi-turn transcripts ran together visually because every user message got the same vertical rhythm regardless of position. Adds a short ─── in the border colour above every user message after the first, so each turn reads as its own block. Height estimator gains a `withSeparator` flag so virtual scrolling pre-allocates the extra two rows (rule + top margin) and avoids a jump on first measurement. While in the area: the busy-indicator duration was padded with `padStart(7)`, leaving five visible spaces between `·` and the digits (`⠋ · 2s`) — especially loud under the verb-less `unicode` style. Drop the padding entirely (`⠋ · 2s`); the model label now shifts a few columns as the duration grows, which is the right trade-off for the minimal indicator styles. The verb-padding test stays; the duration-padding test is removed alongside the function it covered.
…cker dead space (NousResearch#21846) Multi-turn transcripts ran together visually because every user message got the same vertical rhythm regardless of position. Adds a short ─── in the border colour above every user message after the first, so each turn reads as its own block. Height estimator gains a `withSeparator` flag so virtual scrolling pre-allocates the extra two rows (rule + top margin) and avoids a jump on first measurement. While in the area: the busy-indicator duration was padded with `padStart(7)`, leaving five visible spaces between `·` and the digits (`⠋ · 2s`) — especially loud under the verb-less `unicode` style. Drop the padding entirely (`⠋ · 2s`); the model label now shifts a few columns as the duration grows, which is the right trade-off for the minimal indicator styles. The verb-padding test stays; the duration-padding test is removed alongside the function it covered.
…cker dead space (NousResearch#21846) Multi-turn transcripts ran together visually because every user message got the same vertical rhythm regardless of position. Adds a short ─── in the border colour above every user message after the first, so each turn reads as its own block. Height estimator gains a `withSeparator` flag so virtual scrolling pre-allocates the extra two rows (rule + top margin) and avoids a jump on first measurement. While in the area: the busy-indicator duration was padded with `padStart(7)`, leaving five visible spaces between `·` and the digits (`⠋ · 2s`) — especially loud under the verb-less `unicode` style. Drop the padding entirely (`⠋ · 2s`); the model label now shifts a few columns as the duration grows, which is the right trade-off for the minimal indicator styles. The verb-padding test stays; the duration-padding test is removed alongside the function it covered.
…cker dead space (NousResearch#21846) Multi-turn transcripts ran together visually because every user message got the same vertical rhythm regardless of position. Adds a short ─── in the border colour above every user message after the first, so each turn reads as its own block. Height estimator gains a `withSeparator` flag so virtual scrolling pre-allocates the extra two rows (rule + top margin) and avoids a jump on first measurement. While in the area: the busy-indicator duration was padded with `padStart(7)`, leaving five visible spaces between `·` and the digits (`⠋ · 2s`) — especially loud under the verb-less `unicode` style. Drop the padding entirely (`⠋ · 2s`); the model label now shifts a few columns as the duration grows, which is the right trade-off for the minimal indicator styles. The verb-padding test stays; the duration-padding test is removed alongside the function it covered.
Summary
Two small visual polish fixes to the
--tuitranscript and status bar.Inter-turn separator above non-first user messages
Multi-turn transcripts visually ran together because every user message had the same vertical rhythm regardless of position. Now a short
───in the border colour renders above every user message after the first, so each turn reads as its own block.appLayout.tsx— computefirstUserIdxalongside the existinglastUserIdx; render the rule (with a 1-row top margin) inside each row's measured wrapper Box so virtual-scrolling height measurement picks it up automatically.virtualHeights.ts—estimatedMsgHeightgains an optionalwithSeparatorflag that adds 2 rows (rule + top margin), keeping the initial estimate accurate before measurement.useMainApp.ts— passeswithSeparatorfrom the samefirstUserIdxpredicate the renderer uses.Before / after of the rule itself: it's a small
───so it doesn't wrap on narrow terminals.Ticker dead space (
/indicator unicodeand friends)The busy-indicator duration was passed through
padStart(7), which prepended five visible spaces between·and the digits (⠋ · 2s). Particularly loud under the verb-lessunicodestyle where there's nothing else on that side of the rule.appChrome.tsx— droppadTickerDuration/DURATION_PAD_LENentirely;FaceTickernow writes\· ${fmtDuration(...)}`directly, yielding⠋ · 2s`. The model label that follows shifts a few columns as the duration grows ("2s" → "1m 23s"), which is the right trade-off for minimal indicator styles.statusBarTicker.test.ts— removes the duration-padding test alongside the function it covered. Verb-padding tests stay.Test plan
npm run type-check(ui-tui) passesnpm test(ui-tui) — 60 files / 637 tests pass (was 638; one stale test removed)virtualHeights.test.tsassertswithSeparator: trueadds exactly 2 rows───renders above the second/indicator unicode, send a message, confirm spinner reads⠋ · 2swith no leading gap