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
5 changes: 5 additions & 0 deletions .changeset/fix-tui-copy-trailing-spaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix copied TUI chat lines ending with trailing spaces.
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,23 @@ describe('AssistantMessageComponent', () => {
expect(highlightSpy).toHaveBeenCalled();
});

it('does not pad code block lines with trailing spaces', () => {
const component = new AssistantMessageComponent();
component.updateContent('```bash\nbash script.sh \\\n --flag1 \\\n --flag2\n```');

const lines = component.render(80).map(strip);
for (const line of lines) {
expect(line).toBe(line.trimEnd());
}
});

it('marks the rendered zone with OSC 133 markers, once across cache hits', () => {
const component = new AssistantMessageComponent();
component.updateContent('hello');

const lines = component.render(80);
expect(lines[0]).toMatch(/^\u001B\]133;A\u0007/);
expect(lines[lines.length - 1]).toMatch(/^\u001B\]133;B\u0007\u001B\]133;C\u0007/);
expect(lines.at(-1)).toMatch(/^\u001B\]133;B\u0007\u001B\]133;C\u0007/);

const cached = component.render(80);
expect(cached[0]).toBe(lines[0]);
Expand Down
1 change: 1 addition & 0 deletions packages/pi-tui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Never overwrite this directory wholesale when syncing from upstream. Each of the
6. **`src/components/markdown.ts` — `CjkBoundaryUrlTokenizer` autolink CJK boundary**: marked's GFM autolink accepts any non-space characters after the domain and its backpedal strips only ASCII trailing punctuation, so CJK/full-width punctuation right after a bare URL is absorbed into the link text and href (`.../pull/232(本地` renders as one anchor with a CJK target). The `CjkBoundaryUrlTokenizer` subclass (the tokenizer actually registered on the parser) cuts the match at the first CJK punctuation character before the ASCII backpedal; full-width parentheses follow GFM's ASCII-paren rule — balanced pairs stay in the URL (`.../wiki/中华人民共和国(1949年)`, punctuation inside them included), only unbalanced ones terminate the match. `StrictStrikethroughTokenizer` itself stays byte-identical to upstream. Guarding tests: the bare-URL CJK cases in the "Links" group in `test/markdown.test.ts`.
7. **`src/components/editor.ts` — opt-in inline slash autocomplete (`inlineSlashTrigger`)**: when enabled, `/` after whitespace mid-input or at the start of a subsequent line auto-triggers autocomplete (`isAtInlineSlashTrigger`), and typing further token characters (letters, digits, `.`, `-`, `_`, `:`) inside that inline token re-triggers the request (`isInInlineSlashContext`) so the in-flight request from the bare `/` cannot go stale before the menu appears; `:` is required because external skill tokens are shaped `/skill:<name>`. Off by default — prose slashes (paths, fractions) keep upstream behavior. Guarding tests: the "Inline slash trigger" group in `test/editor.test.ts`.
8. **`src/autocomplete.ts` / `src/components/select-list.ts` / `src/components/editor.ts` — `data` on autocomplete items + Enter non-submit for marked completions**: autocomplete items may carry an opaque `data` record; when the selected item's `data.inlineSkill` is set, confirming with Enter applies the completion without submitting the editor (ordinary completions keep upstream Enter-submits behavior). Guarding tests: "does not submit when confirming an inline-marked completion with Enter" and "still submits when confirming an unmarked slash completion with Enter" in `test/editor.test.ts`.
9. **`src/components/text.ts` / `src/components/markdown.ts` — no trailing space padding without background**: when no `bgFn` is applied, lines keep their margins but are not padded to full width, so copied chat lines carry no trailing spaces and shell backslash line continuations survive copy-paste. Padding is unnecessary for display because the main-screen and alt-screen renderers already clear each line with `\x1b[2K` before drawing. Guarding tests: the no-trailing-space cases in `test/tui-render.test.ts`, `test/markdown.test.ts`, and `apps/kimi-code/test/tui/components/messages/assistant-message.test.ts`.

## Acceptance after syncing from upstream

Expand Down
8 changes: 4 additions & 4 deletions packages/pi-tui/src/components/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,10 +434,10 @@ export class Markdown implements Component {
if (bgFn) {
contentLines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));
} else {
// No background - just pad to width
const visibleLen = visibleWidth(lineWithMargins);
const paddingNeeded = Math.max(0, width - visibleLen);
contentLines.push(lineWithMargins + " ".repeat(paddingNeeded));
// No background - keep margins but do not pad to width. Padding
// would fill copied text with trailing spaces and break tokens
// (e.g. shell backslash line continuations).
contentLines.push(lineWithMargins);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Register the new vendored divergence

When packages/pi-tui is next synchronized from upstream, this copy-friendly rendering change can be overwritten because the package guide says every remaining local divergence must be listed and re-verified, but this new text.ts/markdown.ts behavior and its guarding tests were not added to that list. Add a divergence entry covering both components and the new tests so the fix is preserved during re-vendoring.

AGENTS.md reference: packages/pi-tui/AGENTS.md:L3-L7

Useful? React with 👍 / 👎.

}
}

Expand Down
10 changes: 5 additions & 5 deletions packages/pi-tui/src/components/text.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Component } from "../tui.ts";
import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.ts";
import { applyBackgroundToLine, wrapTextWithAnsi } from "../utils.ts";

/**
* Text component - displays multi-line text with word wrapping
Expand Down Expand Up @@ -79,10 +79,10 @@ export class Text implements Component {
if (this.customBgFn) {
contentLines.push(applyBackgroundToLine(lineWithMargins, width, this.customBgFn));
} else {
// No background - just pad to width with spaces
const visibleLen = visibleWidth(lineWithMargins);
const paddingNeeded = Math.max(0, width - visibleLen);
contentLines.push(lineWithMargins + " ".repeat(paddingNeeded));
// No background - keep margins but do not pad to width. Padding
// would fill copied text with trailing spaces and break tokens
// (e.g. shell backslash line continuations).
contentLines.push(lineWithMargins);
}
}

Expand Down
21 changes: 21 additions & 0 deletions packages/pi-tui/test/markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1785,3 +1785,24 @@ describe("Markdown negative width safety", () => {
assert.doesNotThrow(() => markdown.render(-1));
});
});

describe("Markdown copy-friendly rendering", () => {
it("does not pad code block lines with trailing spaces", () => {
const markdown = new Markdown(
"```bash\nbash script.sh \\\n --flag1 \\\n --flag2\n```",
0,
0,
defaultMarkdownTheme,
);
const lines = markdown.render(80).map((line) => stripAnsi(line));
const codeLines = lines.filter((line) => line.startsWith(" "));
assert.ok(codeLines.length >= 3, "should render code lines");
for (const line of codeLines) {
assert.strictEqual(
line,
line.trimEnd(),
`line should not have trailing spaces: ${JSON.stringify(line)}`,
);
}
});
});
16 changes: 16 additions & 0 deletions packages/pi-tui/test/tui-render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,22 @@ describe("Text negative width safety", () => {
});
});

describe("Text copy-friendly rendering", () => {
it("does not fill short lines with trailing spaces", () => {
const text = new Text("hello", 0, 0);
const lines = text.render(80);
assert.strictEqual(lines.length, 1);
assert.strictEqual(lines[0], "hello");
});

it("keeps requested margins without filling the remaining width", () => {
const text = new Text("hello", 2, 0);
const lines = text.render(80);
assert.strictEqual(lines.length, 1);
assert.strictEqual(lines[0], " hello ");
});
});

describe("TUI steady-frame processed-line reuse", () => {
it("writes only the changed line when one component updates in a long transcript", async () => {
const terminal = new LoggingVirtualTerminal(40, 10);
Expand Down