Skip to content

fix(pi-tui): avoid spurious viewport scroll jumps from idle cursor repositioning#1310

Open
bj456736 wants to merge 4 commits into
MoonshotAI:mainfrom
bj456736:fix/pi-tui-hardware-cursor-spurious-scroll
Open

fix(pi-tui): avoid spurious viewport scroll jumps from idle cursor repositioning#1310
bj456736 wants to merge 4 commits into
MoonshotAI:mainfrom
bj456736:fix/pi-tui-hardware-cursor-spurious-scroll

Conversation

@bj456736

@bj456736 bj456736 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Problem

On terminals with Kitty keyboard protocol support (iTerm2, WezTerm, kitty, Ghostty, etc.), releasing modifier keys in a specific order generates a spurious key-release event. This triggers a re-render, which calls positionHardwareCursor() unconditionally — writing ANSI cursor positioning sequences even when the cursor hasn't actually moved. The terminal interprets these sequences as a reason to scroll the viewport back to the cursor.

Reproduction

  1. Run kimi-code in a Kitty-protocol terminal.
  2. Send enough messages so content exceeds terminal height.
  3. Scroll up so the input box is no longer visible.
  4. Press Command + c, but release Command first, then c.
  5. Observe: viewport jumps back down to the input box.

Fix

  • Track hardwareCursorCol alongside hardwareCursorRow.
  • Skip positionHardwareCursor() writes when showHardwareCursor is false and both row and column are unchanged.
  • Reset hardwareCursorCol on force render to stay consistent with the existing hardwareCursorRow reset.

Testing

All 704 pi-tui tests pass.

Upstream reference: earendil-works/pi#3753

qer added 4 commits June 30, 2026 18:22
Closes MoonshotAI#1016

The LLM sometimes passes 'completed' as the status for TodoList items,
but the schema only accepted 'pending' | 'in_progress' | 'done'. This
produced two problems:

1. Validation failed when the model used 'completed'.
2. Even if validation passed, statusMarker() had no case for 'completed'
   and fell through to the unreachable default branch.

Changes:
- Extend TodoStatus union to include 'completed' so it is accepted at
  the type level.
- Map 'completed' -> 'done' in setTodos() so persisted state stays
  clean.
- Handle 'completed' in statusMarker() so it renders as '[done]'.
- Update the markdown description to explicitly warn against using
  'completed'.
- Add a test confirming 'completed' is accepted and mapped to 'done'.
…positioning

Closes MoonshotAI#1307 (upstream earendil-works/pi#3753)

On terminals with Kitty keyboard protocol support, releasing modifier
keys in a specific order (e.g. releasing Command before 'c') generates
a spurious key-release event. This triggers a re-render, which calls
positionHardwareCursor() unconditionally, writing ANSI cursor sequences
even when the cursor hasn't moved. The terminal interprets these
sequences as a reason to scroll the viewport back to the cursor.

Changes:
- Track hardwareCursorCol alongside hardwareCursorRow.
- Skip positionHardwareCursor() writes when showHardwareCursor is false
  and both row and column are unchanged.
- Reset hardwareCursorCol on force render to stay consistent with the
  existing hardwareCursorRow reset.

Tested: all 704 pi-tui tests pass.
@changeset-bot

changeset-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: fef1667

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@fef1667
npx https://pkg.pr.new/@moonshot-ai/kimi-code@fef1667

commit: fef1667

@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: fef1667f65

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

title: z.string().min(1).describe('Short, actionable title for the todo.'),
status: z.enum(['pending', 'in_progress', 'done']).describe('Current status of the todo.'),
status: z
.preprocess((val) => (val === 'completed' ? 'done' : val), z.enum(['pending', 'in_progress', 'done']))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow completed through tool-argument validation

In real tool calls, preflightToolCall validates the provider arguments with AJV against tool.parameters and never runs TodoListInputSchema.safeParse, so this preprocess does not normalize the raw "completed" value before validation. The JSON schema advertised from this schema still describes the inner enum (pending/in_progress/done), so a model call using the compatibility spelling is rejected before setTodos can map it to done; the new test bypasses that path by invoking resolveExecution directly. Please make the JSON schema accept completed as well, or run the Zod parse/normalization before AJV rejects the call.

Useful? React with 👍 / 👎.

Comment on lines +1661 to +1662
if (!this.showHardwareCursor && targetRow === this.hardwareCursorRow && targetCol === this.hardwareCursorCol) {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip cursor writes only after true no-op renders

After a frame rewrites terminal lines, doRender updates hardwareCursorRow to the final rendered row before calling this method, but hardwareCursorCol still contains the previous requested cursor column rather than the column where the write actually left the terminal. When the logical cursor is on that final row and its column is unchanged, this guard returns without emitting the absolute-column move, leaving the physical cursor at the end of the rewritten line instead of at the cursor marker; that breaks IME candidate placement and leaves subsequent cursor state out of sync. Please only suppress the ANSI write for frames that did not already move the terminal cursor, or refresh the tracked column before comparing.

Useful? React with 👍 / 👎.

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.

1 participant