Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c225951
feat(kimi-code): re-baseline pi-tui on upstream v0.84.1 and add fulls…
liruifengv Aug 11, 2026
edb8c56
feat(pi-tui): merge upstream main through 40a3d85 (post-0.84.1)
liruifengv Aug 11, 2026
42a4a5a
fix(kimi-code): keep fullscreen dock from crushing the editor box
liruifengv Aug 11, 2026
d3d388e
docs(kimi-code): document the tui_mode preference in tui.toml
liruifengv Aug 11, 2026
488e622
fix(pi-tui): let terminal focus reports fan out in fullscreen
liruifengv Aug 12, 2026
a80c316
fix(kimi-code): wire openUrl and right-click paste in fullscreen
liruifengv Aug 12, 2026
6ffc65f
feat(kimi-code): fullscreen prompt navigation, exit replay, progress …
liruifengv Aug 12, 2026
4b1cae2
feat(kimi-code): enable Markdown LaTeX rendering with a render_latex …
liruifengv Aug 12, 2026
2c43b31
refactor(kimi-code): gate fullscreen behind KIMI_CODE_TUI_FULL_SCREEN
liruifengv Aug 12, 2026
088c8b9
chore(changesets): clarify fullscreen mode and LaTeX formula entries
liruifengv Aug 12, 2026
001a794
chore(changesets): trim fullscreen mode entry
liruifengv Aug 12, 2026
17cc72d
chore(changesets): trim LaTeX formula entry
liruifengv Aug 12, 2026
a6948ec
chore(changesets): drop redundant kimi-code entries
liruifengv Aug 12, 2026
b000879
Merge remote-tracking branch 'origin/main' into feat/pi-tui-0.84-full…
liruifengv Aug 12, 2026
2146c00
test(kimi-code): add stepRetry to fullscreen layout fixture after mai…
liruifengv Aug 12, 2026
f8397ae
fix(kimi-code): apply render_latex before theme-driven Markdown rebuilds
liruifengv Aug 12, 2026
c839ae6
fix(kimi-code): carry renderLatex through TUI config saves
liruifengv Aug 12, 2026
d69db9a
feat(kimi-code): report tui_mode in lifecycle telemetry
liruifengv Aug 12, 2026
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/cli-markdown-latex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Render LaTeX math formulas (`$…$` / `$$…$$`) in messages as Unicode formulas.
5 changes: 5 additions & 0 deletions .changeset/cli-tui-mode-fullscreen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add an experimental fullscreen TUI mode. Set the `KIMI_CODE_TUI_FULL_SCREEN=1` environment variable to enable it.
5 changes: 5 additions & 0 deletions .changeset/pi-tui-alt-screen-groundwork.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/pi-tui": patch
---

Add fullscreen groundwork to the alternate-screen renderer: `ScrollView.canScroll`, `TuiAltScreen.getLayoutRoot()`, and viewport navigation keys (PageUp/PageDown/Home/End and friends) now fall through to the focused component when the primary scroll view has nothing to scroll. Terminal focus in/out reports are no longer consumed by the viewport input handler, so app-level listeners (focus-aware notifications, clipboard-image hints) keep working in fullscreen.
5 changes: 5 additions & 0 deletions .changeset/pi-tui-upstream-rebaseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/pi-tui": patch
---

Re-baseline the fork on upstream pi-tui v0.84.1 plus upstream main up to `40a3d85` (2026-08-11), which adds the fullscreen transcript search (`ctrl+shift+f`), single-line scroll actions, the ~9-18x alternate-screen render-churn reduction, and an SSH-aware escape-timeout default. The upstream renderer is now split into main-screen and alternate-screen implementations (the `TUI` class is now an interface implemented by `TuiMainScreen` and `TuiAltScreen`), and the Markdown component gained opt-out LaTeX math rendering. All local patches are retained: narrow-terminal hardening, processed-line render caching, editor history hooks, the paste-burst fallback, and multi-root `@` completion. `Editor.setText` accepts a `preservePasteRegistry` option so subclasses can replace text without orphaning live paste markers (upstream resets the registry on every `setText`).
5 changes: 3 additions & 2 deletions apps/kimi-code/src/cli/run-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export async function runShell(
const sessionId = tui.getCurrentSessionId();
const hasContent = tui.hasSessionContent();
setCrashPhase('shutdown');
trackLifecycle('exit', { duration_ms: Date.now() - startedAt });
trackLifecycle('exit', { duration_ms: Date.now() - startedAt, tui_mode: tui.state.ui.mode });
await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS });
const gutter = ' '.repeat(CHROME_GUTTER);
process.stdout.write(`${gutter}Bye!\n`);
Expand Down Expand Up @@ -269,11 +269,12 @@ export async function runShell(
config_ms: configMs,
init_ms: initMs,
mcp_ms: mcpMs,
tui_mode: tui.state.ui.mode,
});
} catch (error) {
removeCrashHandlers();
setCrashPhase('shutdown');
trackLifecycle('exit', { duration_ms: Date.now() - startedAt });
trackLifecycle('exit', { duration_ms: Date.now() - startedAt, tui_mode: tui.state.ui.mode });
await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS });
await harness.close();
throw error;
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/tui/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function currentTuiConfig(host: Pick<SlashCommandHost, 'state'>): TuiConf
theme: host.state.appState.theme,
editorCommand: host.state.appState.editorCommand,
disablePasteBurst: host.state.appState.disablePasteBurst ?? DEFAULT_TUI_CONFIG.disablePasteBurst,
renderLatex: host.state.appState.renderLatex ?? DEFAULT_TUI_CONFIG.renderLatex ?? true,
cacheExpiryHint: host.state.appState.cacheExpiryHint ?? DEFAULT_TUI_CONFIG.cacheExpiryHint,
notifications: host.state.appState.notifications,
upgrade: host.state.appState.upgrade,
Expand Down
3 changes: 2 additions & 1 deletion apps/kimi-code/src/tui/commands/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
import { UsagePanelComponent } from '../components/messages/usage-panel';
import { createMarkdownTheme } from '../theme/pi-tui-theme';
import { formatErrorMessage } from '../utils/event-payload';
import { createMarkdownOptions } from '../utils/markdown-options';
import {
formatPluginSourceLabel,
isOfficialPluginInstall,
Expand Down Expand Up @@ -566,7 +567,7 @@ async function installCapabilityFromPanel(
host.showNotice(`${label} is installed.`);
host.state.transcriptContainer.addChild(new Spacer(1));
host.state.transcriptContainer.addChild(
new Markdown(WEBBRIDGE_POST_INSTALL_MARKDOWN, 2, 0, createMarkdownTheme()),
new Markdown(WEBBRIDGE_POST_INSTALL_MARKDOWN, 2, 0, createMarkdownTheme(), undefined, createMarkdownOptions()),
);
host.state.ui.requestRender();
return;
Expand Down
6 changes: 6 additions & 0 deletions apps/kimi-code/src/tui/commands/reload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { KimiConfig } from '@moonshot-ai/kimi-code-sdk';

import { currentTheme, lightColors } from '#/tui/theme';
import { loadTuiConfig, type TuiConfig } from '../config';
import { setMarkdownRenderLatex } from '../utils/markdown-options';
import type { SlashCommandHost } from './dispatch';
import { setExperimentalFeatures } from './experimental-flags';

Expand Down Expand Up @@ -55,6 +56,10 @@ export async function applyReloadedTuiConfig(
host: SlashCommandHost,
config: TuiConfig,
): Promise<void> {
// Set the LaTeX toggle before applyTheme: theme application invalidates the
// transcript components, which rebuild their Markdown children and copy the
// options at construction — so the new value must be live by then.
setMarkdownRenderLatex(config.renderLatex ?? true);
const resolved = config.theme === 'auto'
? (currentTheme.palette === lightColors ? 'light' : 'dark')
: undefined;
Expand All @@ -63,6 +68,7 @@ export async function applyReloadedTuiConfig(
host.setAppState({
editorCommand: config.editorCommand,
disablePasteBurst: config.disablePasteBurst,
renderLatex: config.renderLatex,
cacheExpiryHint: config.cacheExpiryHint,
notifications: config.notifications,
upgrade: config.upgrade,
Expand Down
5 changes: 4 additions & 1 deletion apps/kimi-code/src/tui/components/chrome/gutter-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { Container } from '@moonshot-ai/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';

import { prefixPreservingOsc133Zone } from '#/tui/utils/osc133';
import { isRenderCacheEnabled } from '#/tui/utils/render-cache';

interface TranscriptRenderCache {
Expand Down Expand Up @@ -68,7 +69,9 @@ export class GutterContainer extends Container {
prefixed.push(cache.prefixed[i]!);
} else {
allReused = false;
prefixed.push(lines.map((line) => lead + line));
// OSC 133 zone markers must stay at byte 0 for the fullscreen
// renderer's prompt navigation, so the gutter goes after them.
prefixed.push(lines.map((line) => prefixPreservingOsc133Zone(line, lead)));
}
i++;
}
Expand Down
4 changes: 3 additions & 1 deletion apps/kimi-code/src/tui/components/editor/custom-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,9 @@ export class CustomEditor extends Editor {
const text = this.getText();
const offset = lines.slice(0, line).reduce((sum, l) => sum + l.length + 1, 0) + start;
const newText = text.slice(0, offset) + content + text.slice(offset + match[0].length);
this.setText(newText);
// Keep the paste registry intact: the text still holds other live markers
// whose entries a plain setText would drop (upstream resets the registry).
this.setText(newText, { preservePasteRegistry: true });
return true;
}
return false;
Expand Down
15 changes: 13 additions & 2 deletions apps/kimi-code/src/tui/components/messages/assistant-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { MESSAGE_INDENT } from '#/tui/constant/rendering';
import { STATUS_BULLET } from '#/tui/constant/symbols';
import { currentTheme } from '#/tui/theme';
import { createMarkdownTheme } from '#/tui/theme/pi-tui-theme';
import { createMarkdownOptions } from '#/tui/utils/markdown-options';
import { markOsc133Zone } from '#/tui/utils/osc133';
import { isRenderCacheEnabled } from '#/tui/utils/render-cache';

type AssistantMarkdownOptions = {
Expand Down Expand Up @@ -61,7 +63,14 @@ export class AssistantMessageComponent implements Component {

if (this.markdown === undefined || this.markdownTransient !== transient) {
this.contentContainer.clear();
this.markdown = new Markdown(displayText, 0, 0, createMarkdownTheme({ transient }));
this.markdown = new Markdown(
displayText,
0,
0,
createMarkdownTheme({ transient }),
undefined,
createMarkdownOptions(),
);
this.markdownTransient = transient;
this.contentContainer.addChild(this.markdown);
return;
Expand All @@ -84,6 +93,8 @@ export class AssistantMessageComponent implements Component {
0,
0,
createMarkdownTheme({ transient: this.lastTransient }),
undefined,
createMarkdownOptions(),
);
this.markdownTransient = this.lastTransient;
this.contentContainer.addChild(this.markdown);
Expand Down Expand Up @@ -114,7 +125,7 @@ export class AssistantMessageComponent implements Component {
i === 0 && this.showBullet ? currentTheme.fg('text', STATUS_BULLET) : MESSAGE_INDENT;
lines.push(p + contentLines[i]);
}
const rendered = lines.map((line) => truncateToWidth(line, safeWidth, '…'));
const rendered = markOsc133Zone(lines.map((line) => truncateToWidth(line, safeWidth, '…')));
if (isRenderCacheEnabled()) {
this.renderCache = { width: safeWidth, lines: rendered };
}
Expand Down
3 changes: 2 additions & 1 deletion apps/kimi-code/src/tui/components/messages/plan-box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { pathToFileURL } from 'node:url';
import { Markdown, truncateToWidth, visibleWidth, type Component, type MarkdownTheme } from '@moonshot-ai/pi-tui';
import chalk from 'chalk';

import { createMarkdownOptions } from '#/tui/utils/markdown-options';
import { toTerminalHyperlink } from '#/utils/terminal-hyperlink';

const LEFT_MARGIN = 2; // two-space indent matching other tool call children
Expand Down Expand Up @@ -41,7 +42,7 @@ export class PlanBoxComponent implements Component {
// parse + wrap output keyed on (text, width), so reusing the same
// instance means repeated render() calls from the parent Container
// hit the cache instead of re-parsing on every frame.
this.markdown = new Markdown(plan.trim(), 0, 0, markdownTheme);
this.markdown = new Markdown(plan.trim(), 0, 0, markdownTheme, undefined, createMarkdownOptions());
this.status = opts?.status;
}

Expand Down
21 changes: 12 additions & 9 deletions apps/kimi-code/src/tui/components/messages/user-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ImageThumbnail } from '#/tui/components/media/image-thumbnail';
import { USER_MESSAGE_BULLET } from '#/tui/constant/symbols';
import { currentTheme } from '#/tui/theme';
import type { ImageAttachment } from '#/tui/utils/image-attachment-store';
import { markOsc133Zone } from '#/tui/utils/osc133';
import { isRenderCacheEnabled } from '#/tui/utils/render-cache';

export class UserMessageComponent implements Component {
Expand Down Expand Up @@ -77,15 +78,17 @@ export class UserMessageComponent implements Component {
}
}

const rendered = lines.map((line) => {
// Inline image sequences (Kitty / iTerm2) carry their own placement
// information and have zero visible width, but pi-tui's truncateToWidth
// treats the embedded base64 payload as visible text and would chop the
// escape sequence in half, leaving garbage like "0m...". Skip truncation
// for those lines; the image itself already respects maxWidthCells.
if (isImageLine(line)) return line;
return truncateToWidth(line, safeWidth, '…');
});
const rendered = markOsc133Zone(
lines.map((line) => {
// Inline image sequences (Kitty / iTerm2) carry their own placement
// information and have zero visible width, but pi-tui's truncateToWidth
// treats the embedded base64 payload as visible text and would chop the
// escape sequence in half, leaving garbage like "0m...". Skip truncation
// for those lines; the image itself already respects maxWidthCells.
if (isImageLine(line)) return line;
return truncateToWidth(line, safeWidth, '…');
}),
);
if (isRenderCacheEnabled()) {
this.renderCache = { width: safeWidth, lines: rendered };
}
Expand Down
5 changes: 4 additions & 1 deletion apps/kimi-code/src/tui/components/panes/btw-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import chalk from 'chalk';

import { THINKING_PREVIEW_LINES } from '../../constant/rendering';
import { currentTheme } from '../../theme';
import { createMarkdownOptions } from '../../utils/markdown-options';

type BtwPanelPhase = 'running' | 'done' | 'failed';

Expand Down Expand Up @@ -195,7 +196,9 @@ export class BtwPanelComponent implements Component {
const answer = turn.answer.trim();
const thinking = turn.thinking.trim();
if (answer.length > 0) {
lines.push(...new Markdown(answer, 0, 0, this.options.markdownTheme).render(width));
lines.push(
...new Markdown(answer, 0, 0, this.options.markdownTheme, undefined, createMarkdownOptions()).render(width),
);
} else if (thinking.length > 0) {
const thinkingLines = new Text(chalk.hex(currentTheme.palette.textDim)(thinking), 0, 0).render(
width,
Expand Down
7 changes: 7 additions & 0 deletions apps/kimi-code/src/tui/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const DEFAULT_STATUS_LINE_CONFIG: StatusLineConfig = {

export const TuiConfigFileSchema = z.object({
theme: TuiThemeSchema.optional(),
render_latex: z.boolean().optional(),
disable_paste_burst: z.boolean().optional(),
cache_expiry_hint: z.boolean().optional(),
editor: z
Expand All @@ -76,6 +77,9 @@ export const TuiConfigFileSchema = z.object({

export const TuiConfigSchema = z.object({
theme: TuiThemeSchema,
/** LaTeX math rendering in Markdown; optional only so older hand-built test
* fixtures still typecheck. */
renderLatex: z.boolean().optional(),
disablePasteBurst: z.boolean(),
/** Present in every normalized config; optional only so hand-built test
* fixtures from before this field existed still typecheck. */
Expand Down Expand Up @@ -104,6 +108,7 @@ export const DEFAULT_UPGRADE_PREFERENCES: UpgradePreferences = {

export const DEFAULT_TUI_CONFIG: TuiConfig = TuiConfigSchema.parse({
theme: 'auto',
renderLatex: true,
disablePasteBurst: false,
cacheExpiryHint: true,
editorCommand: null,
Expand Down Expand Up @@ -190,6 +195,7 @@ export function normalizeTuiConfig(
.map((item) => item as StatusLineItem) ?? null;
return TuiConfigSchema.parse({
theme: config.theme ?? DEFAULT_TUI_CONFIG.theme,
renderLatex: config.render_latex ?? DEFAULT_TUI_CONFIG.renderLatex,
disablePasteBurst: config.disable_paste_burst ?? DEFAULT_TUI_CONFIG.disablePasteBurst,
cacheExpiryHint: config.cache_expiry_hint ?? DEFAULT_TUI_CONFIG.cacheExpiryHint,
editorCommand: command === undefined || command.length === 0 ? null : command,
Expand Down Expand Up @@ -239,6 +245,7 @@ export function renderTuiConfig(config: TuiConfig): string {
# Agent/runtime settings stay in ~/.kimi-code/config.toml.

theme = "${escapeTomlBasicString(config.theme)}" # "auto" | "dark" | "light" | custom theme name
render_latex = ${String(config.renderLatex !== false)} # false keeps LaTeX math in assistant messages as raw source
Comment thread
liruifengv marked this conversation as resolved.
disable_paste_burst = ${String(config.disablePasteBurst)} # true disables non-bracketed paste-burst fallback
cache_expiry_hint = ${String(config.cacheExpiryHint !== false)} # false disables the "cache expired" dialog on resume / idle submit

Expand Down
9 changes: 9 additions & 0 deletions apps/kimi-code/src/tui/constant/rendering.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
// Continuation indent for transcript rows that use a two-cell leading marker.
export const MESSAGE_INDENT = ' ';

// OSC 133 semantic-zone markers (FinalTerm/shell-integration protocol):
// zero-width escape sequences prefixed onto the first/last rendered line of
// transcript messages. The fullscreen renderer strips them at paint and uses
// the A marker for previous/next-prompt navigation (Ctrl-Shift-Up/Down); in
// regular mode they pass through to native scrollback invisibly.
export const OSC133_ZONE_START = '\x1b]133;A\x07';
export const OSC133_ZONE_END = '\x1b]133;B\x07';
export const OSC133_ZONE_FINAL = '\x1b]133;C\x07';

// Outer left/right padding applied to the transcript, panels, and the
// statusline so the chrome's left edge lines up with the input box's
// interior (the `>` prompt). The editor itself stays at column 0 — its
Expand Down
11 changes: 10 additions & 1 deletion apps/kimi-code/src/tui/controllers/editor-keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export interface EditorKeyboardHost {
handleInputModeChange(mode: 'prompt' | 'bash'): void;
clearQueuedMessages(): void;
setExternalEditorRunning(running: boolean): void;
updateActivityPane(): void;
}

export class EditorKeyboardController {
Expand Down Expand Up @@ -525,7 +526,10 @@ export class EditorKeyboardController {
}
this.host.setExternalEditorRunning(true);
const seed = state.editor.getExpandedText?.() ?? state.editor.getText();
state.ui.stop();
// Fullscreen: a plain stop() would replay the whole transcript into the
// main screen on exit; the external editor only needs the alternate
// screen released, so preserve the screen instead.
state.ui.stop({ preserveScreen: state.ui.mode === 'fullscreen' ? true : undefined });
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
Expand All @@ -544,6 +548,11 @@ export class EditorKeyboardController {
state.ui.start();
state.ui.setFocus(state.editor);
state.ui.requestRender(true);
// terminal.stop() cleared the OSC 9;4 progress indicator while the
// app-side progressActive flag still reads true; resync so a turn that
// was streaming while the editor was open gets its progress back.
state.terminalState.progressActive = false;
this.host.updateActivityPane();
this.host.setExternalEditorRunning(false);
}
}
Expand Down
5 changes: 4 additions & 1 deletion apps/kimi-code/src/tui/controllers/subagent-event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,8 +662,11 @@ export class SubAgentEventHandler {
}

const width = Math.floor(terminalColumns);
const dock = state.dockContainer;
// Fullscreen: the root children are empty (layout root holds a ScrollView +
// dock); the chrome below the transcript is the dock's children instead.
const rowsAfterSwarm = renderedRowsAfterChild(
state.ui.children,
dock !== undefined ? [state.transcriptContainer, ...dock.children] : state.ui.children,
state.transcriptContainer,
width,
);
Expand Down
Loading
Loading