Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Only write entries that are worth mentioning to users.

## Unreleased

- Shell: Fix inline diff highlights misaligned on lines containing tabs — the sequence matcher now compares tab-expanded text so highlight offsets match the rendered output

## 1.29.0 (2026-04-01)

- Core: Support hierarchical `AGENTS.md` loading — the CLI now discovers and merges `AGENTS.md` files from the git project root down to the working directory, including `.kimi/AGENTS.md` at each level; deeper files take priority under a 32 KiB budget cap, ensuring the most specific instructions are never truncated
Expand Down
10 changes: 5 additions & 5 deletions src/kimi_cli/utils/rich/diff_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,13 @@ def _apply_inline_diff(
colors = get_diff_colors()
paired = min(len(del_lines), len(add_lines))
for j in range(paired):
old_code = del_lines[j].code
new_code = add_lines[j].code
sm = SequenceMatcher(None, old_code, new_code)
old_text = _highlight(highlighter, del_lines[j].code)
new_text = _highlight(highlighter, add_lines[j].code)
# Compare the highlighted plain text so offsets match the Text objects
# (highlighting may expand tabs, so raw .code offsets would be wrong).
sm = SequenceMatcher(None, old_text.plain, new_text.plain)
Comment on lines +158 to +162
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 Avoid re-highlighting unpaired line pairs

_apply_inline_diff now highlights both lines before checking the similarity ratio, but when sm.ratio() < _INLINE_DIFF_MIN_RATIO it continues without storing those Text objects; _highlight_hunk then highlights the same lines again in its second pass (if dl.content is None). In replace blocks with many low-similarity pairs, this doubles syntax-highlighting work and can significantly slow rendering for large diffs.

Useful? React with 👍 / 👎.

if sm.ratio() < _INLINE_DIFF_MIN_RATIO:
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

In _apply_inline_diff, old_text/new_text are highlighted before the similarity threshold check. When sm.ratio() < _INLINE_DIFF_MIN_RATIO, those Text objects are discarded and the same lines will be highlighted again in _highlight_hunk’s second pass (since content stays None). Consider either (a) setting del_lines[j].content / add_lines[j].content even when skipping inline pairing (leaving is_inline_paired=False), or (b) doing the ratio check on tab-expanded raw strings first and only calling _highlight() when the pair will actually be inline-diffed, to avoid redundant highlighting work.

Suggested change
if sm.ratio() < _INLINE_DIFF_MIN_RATIO:
if sm.ratio() < _INLINE_DIFF_MIN_RATIO:
# Too dissimilar for inline word-level diff; still reuse the
# already-highlighted Text objects to avoid re-highlighting
# these lines in _highlight_hunk's second pass.
del_lines[j].content = old_text
add_lines[j].content = new_text

Copilot uses AI. Check for mistakes.
continue
old_text = _highlight(highlighter, old_code)
new_text = _highlight(highlighter, new_code)
for op, i1, i2, j1, j2 in sm.get_opcodes():
if op in ("delete", "replace"):
old_text.stylize(colors.del_hl, i1, i2)
Expand Down
32 changes: 32 additions & 0 deletions tests/utils/test_diff_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,38 @@ def test_unequal_block_sizes_partial_pairing(self) -> None:
# 3rd delete not paired
assert not deletes[2].is_inline_paired

def test_inline_diff_with_tabs(self) -> None:
"""Inline highlight offsets must account for tab-to-space expansion."""
old = "\told_value = 1"
new = "\tnew_value = 2"
hunks = _build_diff_lines(old, new, 1, 1)
hl = _make_highlighter("test.py")
_highlight_hunk(hl, hunks[0])
deletes = [dl for dl in hunks[0] if dl.kind == DiffLineKind.DELETE]
adds = [dl for dl in hunks[0] if dl.kind == DiffLineKind.ADD]
assert deletes[0].is_inline_paired
assert adds[0].is_inline_paired
del_plain = deletes[0].content.plain
add_plain = adds[0].content.plain
assert "old_value" in del_plain
assert "new_value" in add_plain
# Verify the highlight spans cover the actual changed words,
# not characters shifted by unexpanded-tab offsets.
from kimi_cli.utils.rich.diff_render import get_diff_colors

colors = get_diff_colors()
del_hl_spans = [
(s.start, s.end) for s in deletes[0].content._spans if s.style == colors.del_hl
]
add_hl_spans = [
(s.start, s.end) for s in adds[0].content._spans if s.style == colors.add_hl
Comment on lines +187 to +190
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The new test reaches into Rich internals via Text._spans. That private attribute isn’t part of Rich’s stable API and may break across Rich versions. Prefer using the public Text.spans (or another public accessor) to inspect spans/styles for the assertion.

Suggested change
(s.start, s.end) for s in deletes[0].content._spans if s.style == colors.del_hl
]
add_hl_spans = [
(s.start, s.end) for s in adds[0].content._spans if s.style == colors.add_hl
(s.start, s.end) for s in deletes[0].content.spans if s.style == colors.del_hl
]
add_hl_spans = [
(s.start, s.end) for s in adds[0].content.spans if s.style == colors.add_hl

Copilot uses AI. Check for mistakes.
]
# The highlighted region in the old line must cover "old" (from old_value)
del_highlighted = "".join(del_plain[s:e] for s, e in del_hl_spans)
add_highlighted = "".join(add_plain[s:e] for s, e in add_hl_spans)
assert "old" in del_highlighted, f"expected 'old' in highlighted text: {del_highlighted!r}"
assert "new" in add_highlighted, f"expected 'new' in highlighted text: {add_highlighted!r}"


# ---------------------------------------------------------------------------
# collect_diff_hunks
Expand Down
Loading