Skip to content
Merged
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/warn-tmux-key-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Warn tmux users when extended key settings may prevent modified Enter shortcuts from working.
9 changes: 9 additions & 0 deletions apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@
import { notifyTerminalOnce } from './utils/terminal-notification';
import { createTerminalState, type TerminalState } from './utils/terminal-state';
import { installTerminalThemeTracking } from './utils/terminal-theme';
import { detectTmuxKeyboardWarning } from './utils/tmux-keyboard';
import { nextTranscriptId } from './utils/transcript-id';

export interface KimiTUIStartupInput {
Expand Down Expand Up @@ -763,6 +764,7 @@
this.showStatus(this.state.startupNotice);
this.state.startupNotice = undefined;
}
void this.showTmuxKeyboardWarningIfNeeded();
if (this.state.startupState === 'picker') {
void this.bootstrapFromPicker();
// resumeSession (fired on picker select) owns post-pick init; nothing
Expand All @@ -786,6 +788,13 @@
void this.refreshSkillCommands(this.session);
}

// Warns tmux users when modified Enter shortcuts are likely to be swallowed.
private async showTmuxKeyboardWarningIfNeeded(): Promise<void> {
const warning = await detectTmuxKeyboardWarning();
if (warning === undefined || this.aborted) return;
this.showStatus(warning, this.state.theme.colors.warning);
}

// Creates or resumes the startup session and reports whether history should replay.
private async init(): Promise<boolean> {
await this.refreshAvailableModels();
Expand Down Expand Up @@ -1305,7 +1314,7 @@
isCompacting: this.state.appState.isCompacting,
});

switch (intent.kind) {

Check warning on line 1317 in apps/kimi-code/src/tui/kimi-tui.ts

View workflow job for this annotation

GitHub Actions / lint

typescript-eslint(switch-exhaustiveness-check)

Switch is not exhaustive. Cases not matched: "invalid"
case 'not-command':
return;
case 'blocked':
Expand Down
68 changes: 68 additions & 0 deletions apps/kimi-code/src/tui/utils/tmux-keyboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { spawn } from 'node:child_process';

const TMUX_QUERY_TIMEOUT_MS = 2000;

export const TMUX_EXTENDED_KEYS_OFF_WARNING =
'tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.';

export const TMUX_EXTENDED_KEYS_FORMAT_XTERM_WARNING =
'tmux extended-keys-format is xterm. Kimi Code works best with csi-u. Add `set -g extended-keys-format csi-u` to ~/.tmux.conf and restart tmux.';

export type TmuxOptionReader = (option: string) => Promise<string | undefined>;

export async function detectTmuxKeyboardWarning(
env: NodeJS.ProcessEnv = process.env,
readTmuxOption: TmuxOptionReader = readTmuxOptionFromProcess,
): Promise<string | undefined> {
if ((env['TMUX'] ?? '').length === 0) return undefined;

const [extendedKeys, extendedKeysFormat] = await Promise.all([
readTmuxOption('extended-keys'),
readTmuxOption('extended-keys-format'),
]);

if (extendedKeys === undefined) return undefined;

if (extendedKeys !== 'on' && extendedKeys !== 'always') {
return TMUX_EXTENDED_KEYS_OFF_WARNING;
}

if (extendedKeysFormat === 'xterm') {
return TMUX_EXTENDED_KEYS_FORMAT_XTERM_WARNING;
}

return undefined;
}

function readTmuxOptionFromProcess(option: string): Promise<string | undefined> {
return new Promise((resolve) => {
const proc = spawn('tmux', ['show', '-gv', option], {
stdio: ['ignore', 'pipe', 'ignore'],
});
let stdout = '';
let settled = false;
let timer: NodeJS.Timeout;

const finish = (value: string | undefined) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve(value);

Check warning on line 50 in apps/kimi-code/src/tui/utils/tmux-keyboard.ts

View workflow job for this annotation

GitHub Actions / lint

eslint-plugin-promise(no-multiple-resolved)

Promise should not be resolved multiple times. Promise is already resolved on line 49.
};

timer = setTimeout(() => {
proc.kill();
finish(undefined);
}, TMUX_QUERY_TIMEOUT_MS);

proc.stdout?.on('data', (data: Buffer) => {
stdout += data.toString('utf8');
});
proc.on('error', () => {
finish(undefined);
});
proc.on('close', (code) => {
finish(code === 0 ? stdout.trim() : undefined);
});
});
}
71 changes: 71 additions & 0 deletions apps/kimi-code/test/tui/tmux-keyboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, it, vi } from 'vitest';

import {
detectTmuxKeyboardWarning,
TMUX_EXTENDED_KEYS_FORMAT_XTERM_WARNING,
TMUX_EXTENDED_KEYS_OFF_WARNING,
type TmuxOptionReader,
} from '#/tui/utils/tmux-keyboard';

function optionReader(values: Record<string, string | undefined>): TmuxOptionReader {
return vi.fn(async (option: string) => values[option]);
}

describe('tmux keyboard setup detection', () => {
it('skips checks outside tmux', async () => {
const readOption = optionReader({});

await expect(detectTmuxKeyboardWarning({}, readOption)).resolves.toBeUndefined();

expect(readOption).not.toHaveBeenCalled();
});

it('does not warn when tmux options cannot be queried', async () => {
const readOption = optionReader({
'extended-keys': undefined,
'extended-keys-format': undefined,
});

await expect(
detectTmuxKeyboardWarning({ TMUX: '/tmp/tmux/default,123,0' }, readOption),
).resolves.toBeUndefined();
});

it('warns when extended-keys is off', async () => {
const readOption = optionReader({
'extended-keys': 'off',
'extended-keys-format': 'csi-u',
});

await expect(
detectTmuxKeyboardWarning({ TMUX: '/tmp/tmux/default,123,0' }, readOption),
).resolves.toBe(TMUX_EXTENDED_KEYS_OFF_WARNING);
});

it('warns when extended-keys-format is xterm', async () => {
const readOption = optionReader({
'extended-keys': 'on',
'extended-keys-format': 'xterm',
});

await expect(
detectTmuxKeyboardWarning({ TMUX: '/tmp/tmux/default,123,0' }, readOption),
).resolves.toBe(TMUX_EXTENDED_KEYS_FORMAT_XTERM_WARNING);
});

it('accepts on and always with csi-u or absent format', async () => {
await expect(
detectTmuxKeyboardWarning(
{ TMUX: '/tmp/tmux/default,123,0' },
optionReader({ 'extended-keys': 'on', 'extended-keys-format': 'csi-u' }),
),
).resolves.toBeUndefined();

await expect(
detectTmuxKeyboardWarning(
{ TMUX: '/tmp/tmux/default,123,0' },
optionReader({ 'extended-keys': 'always', 'extended-keys-format': undefined }),
),
).resolves.toBeUndefined();
});
});
Loading