Skip to content

feat(tui): pin the last user message at the top of the viewport - #2491

Closed
lucastononro wants to merge 3 commits into
MoonshotAI:mainfrom
lucastononro:feat/pin-last-user-message
Closed

feat(tui): pin the last user message at the top of the viewport#2491
lucastononro wants to merge 3 commits into
MoonshotAI:mainfrom
lucastononro:feat/pin-last-user-message

Conversation

@lucastononro

@lucastononro lucastononro commented Jul 31, 2026

Copy link
Copy Markdown

Related Issue

Resolve #2490

Problem

During long agent runs the user's prompt quickly scrolls into terminal scrollback. Claude Code solves this nicely by keeping the last sent message pinned at the top of the viewport; Kimi Code had no equivalent, so you lose sight of the instruction that kicked off the current work.

What changed

The last sent message now stays visible as a subtle band pinned to the top of the terminal viewport once it scrolls off — kimi-blue text with a thin muted rule, capped at 3 lines with an ellipsis. It never duplicates on-screen content (hidden while the original message is still visible), updates on every send, and clears on /clear, /new, and session switches. Opt out with pin_last_user_message = false in tui.toml (default true, live-reloadable via /reload).

Evidence video (42s, real terminal, stub session — no API calls): https://github.com/lucastononro/kimi-code/releases/download/pin-last-user-message-evidence/evidence.mp4

A dev build of this branch running on macOS (Terminal.app, real session) — prompt pinned at the top while a 300-line reply streams underneath:

How it works

pi-tui renders the whole component tree as a flat line buffer whose tail is the viewport — there is no top-static region. The one existing mechanism that paints at a fixed viewport position regardless of scroll is the overlay system (TUI.showOverlay, composited per frame), so the pin is implemented as a non-capturing, top-anchored, full-width overlay:

  • packages/pi-tui/src/tui.ts — two read-only accessors, getViewportTop() and getContentHeight(), so components can read the engine's scroll state. No render-path changes.
  • PinnedUserMessageComponent (new) — snapshots the message text (the transcript window may trim the original UserMessageComponent), and returns zero lines until the original message has scrolled above the viewport. Visibility is anchored to the message's actual position: the transcript container's rendered height right after the append (cheap via child render caches), not the engine's stale whole-buffer height.
  • KimiTUI — snapshots the text on every live send (normal, mid-stream steer, and queued-then-sent flows), lazily mounts the overlay on first send, and clears the pin on /clear, /new, and session switches. Dialogs (higher focus order) still stack above the pin.
  • Config + docs — new pin_last_user_message key in tui.toml, documented in the configuration reference (en/zh).

Trade-off (accepted)

While the pin is visible, frames that append transcript lines shift the overlay's absolute row, so the differential renderer repaints from the top of the viewport on those frames (in-place updates like spinners and streaming deltas on existing lines stay cheap). Output is wrapped in synchronized-output markers, so no tearing — same cost profile as any open dialog overlay today. A DECSTBM scroll-region engine rewrite would avoid it but risks breaking scrollback semantics and the cursor math.

Validation

  • apps/kimi-code vitest suite: 2479 passed (incl. 7 new component tests + 4 new flow tests; config fixtures updated)
  • packages/pi-tui node:test suite: 739 passed (incl. 3 new viewport-accessor tests)
  • pnpm lint clean, monorepo pnpm typecheck clean
  • Runtime evidence above: pin appears only after the prompt scrolls off, transcript scrolls underneath, follow-up re-pins

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

While the agent works and the transcript scrolls, the user's most
recently sent message now stays visible as a band pinned to the top of
the terminal viewport — same idea as Claude Code's pinned prompt — so
the prompt that kicked off the current work is always in view.

- pi-tui: expose getViewportTop()/getContentHeight() so components can
  read the engine's scroll state.
- New PinnedUserMessageComponent, mounted as a non-capturing, top
  anchored, full-width overlay (the existing compositing path used by
  dialogs; dialogs still stack above it). It snapshots the message text
  (the transcript window may trim the original entry), caps the band at
  3 lines with an ellipsis, and stays hidden until the original message
  has scrolled above the viewport so it never duplicates on-screen
  content.
- KimiTUI snapshots the text on every live send (normal, steered, and
  queued-then-sent paths) and clears the pin on /clear, /new, and
  session switches.
- New pin_last_user_message toggle in tui.toml (default true),
  live-reloadable via /reload.

Validation: apps/kimi-code vitest suite (2479 passed), pi-tui node:test
suite (739 passed), oxlint clean, monorepo typecheck clean, plus a
recorded runtime demo of the feature in a real terminal.
@changeset-bot

changeset-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7369a98

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@moonshot-ai/pi-tui Patch
@moonshot-ai/kimi-code Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dddd03ccbe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/kimi-code/src/tui/kimi-tui.ts Outdated
Comment thread apps/kimi-code/src/tui/config.ts
Restyle the pin from bold roleUser amber on a solid gray band to
regular-weight primary (kimi blue) text on the terminal's own
background with a thin muted rule underneath — lighter, and matches
the rest of the chrome.
…data

Address review feedback:

- Anchor the pin's visibility to the message's actual buffer position
  (the transcript container's rendered height right after the append)
  instead of the engine's whole-buffer content height, which is a frame
  stale and also counts the bottom chrome and overlay padding — this
  could displace the anchor by several rows after batched steer items
  or transcript trimming.
- Add changesets per the gen-changesets rules: a minor CLI entry for
  the feature and a separate pi-tui patch entry for the scroll-state
  accessors (the two changelogs must not mix).
- Document pin_last_user_message in the tui.toml reference (en/zh).
@liruifengv liruifengv closed this Aug 1, 2026
@liruifengv

Copy link
Copy Markdown
Collaborator

Sorry, we can't accept this feature

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.

feat(tui): pin the last user message at the top of the viewport

2 participants