Skip to content

feat(tui): segment turns with rule above non-first user msgs; trim ticker dead space#21846

Merged
OutThisLife merged 1 commit into
mainfrom
bb/tui-user-msg-separator
May 8, 2026
Merged

feat(tui): segment turns with rule above non-first user msgs; trim ticker dead space#21846
OutThisLife merged 1 commit into
mainfrom
bb/tui-user-msg-separator

Conversation

@OutThisLife

Copy link
Copy Markdown
Collaborator

Summary

Two small visual polish fixes to the --tui transcript 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 — compute firstUserIdx alongside the existing lastUserIdx; 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.tsestimatedMsgHeight gains an optional withSeparator flag that adds 2 rows (rule + top margin), keeping the initial estimate accurate before measurement.
  • useMainApp.ts — passes withSeparator from the same firstUserIdx predicate 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 unicode and 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-less unicode style where there's nothing else on that side of the rule.

  • appChrome.tsx — drop padTickerDuration / DURATION_PAD_LEN entirely; FaceTicker now 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) passes
  • npm test (ui-tui) — 60 files / 637 tests pass (was 638; one stale test removed)
  • New test in virtualHeights.test.ts asserts withSeparator: true adds exactly 2 rows
  • Manual: open a session, send two user messages, confirm ─── renders above the second
  • Manual: /indicator unicode, send a message, confirm spinner reads ⠋ · 2s with no leading gap

…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.
@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: bb/tui-user-msg-separator vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 7737 on HEAD, 7688 on base (🆕 +49)

🆕 New issues (31):

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 estimatedMsgHeight with an optional withSeparator flag 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.

@OutThisLife OutThisLife merged commit 42f9234 into main May 8, 2026
13 of 14 checks passed
@OutThisLife OutThisLife deleted the bb/tui-user-msg-separator branch May 8, 2026 12:12
RationallyPrime pushed a commit to RationallyPrime/hermes-agent that referenced this pull request May 8, 2026
…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.
JZKK720 pushed a commit to JZKK720/hermes-agent that referenced this pull request May 11, 2026
…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.
rmulligan pushed a commit to rmulligan/hermes-agent that referenced this pull request May 11, 2026
…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.
JinyuID pushed a commit to JinyuID/hermes-agent that referenced this pull request May 11, 2026
…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.
jsboige pushed a commit to jsboige/hermes-agent that referenced this pull request May 14, 2026
…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.
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…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.
Egavasyug pushed a commit to Egavasyug/hermes-agent that referenced this pull request Jun 10, 2026
…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.
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.

2 participants