mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-17 20:55:34 +00:00
feat(tui): print the fork resume command and copy it to the clipboard (#2940)
Some checks are pending
CI / build (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
CI / test (3) (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-vscode-legacy (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Some checks are pending
CI / build (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
CI / test (3) (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-vscode-legacy (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(tui): print the fork resume command and copy it to the clipboard * fix(tui): use pushd in the Windows fork resume command so it switches drives cmd.exe's `cd` only updates the target drive's remembered directory, so a terminal on another drive would run `kimi --resume` in the wrong working directory. `pushd` switches drive + directory in both cmd.exe and PowerShell (`cd /d` would break PowerShell). Addresses the Codex review comment. * fix(tui): label OSC 52 clipboard delivery as unverified after fork copyTextToClipboard falls back to an OSC 52 escape when no native clipboard provider works; terminals without OSC 52 support silently drop the sequence, so only native delivery may claim success. Matches the wording convention of /copy. Addresses the Codex review comment. --------- Co-authored-by: bj456736 <bj456736@users.noreply.github.com>
This commit is contained in:
parent
d96cd03770
commit
6b72345f8b
5 changed files with 119 additions and 2 deletions
5
.changeset/fork-print-resume-command.md
Normal file
5
.changeset/fork-print-resume-command.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Print the full `kimi --resume` command after `/fork` and copy it to the clipboard, so the fork can be entered directly from a new CLI process.
|
||||
|
|
@ -5,7 +5,9 @@ import { pathToFileURL } from 'node:url';
|
|||
import type { Session } from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
import { detectInstallSource } from '#/cli/update/source';
|
||||
import { copyTextToClipboard } from '#/utils/clipboard/clipboard-text';
|
||||
import { detectShellEnvironment } from '#/utils/process/shell-env';
|
||||
import { quoteShellArg } from '#/utils/shell-quote';
|
||||
import { toTerminalHyperlink } from '#/utils/terminal-hyperlink';
|
||||
import { LLM_NOT_SET_MESSAGE, NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui';
|
||||
import { isAbortError } from '../utils/errors';
|
||||
|
|
@ -76,9 +78,26 @@ export async function handleForkCommand(host: SlashCommandHost, args: string): P
|
|||
}
|
||||
// Stay in the source session: switching to the fork would close the
|
||||
// source, killing its in-flight turn and background tasks. The fork is
|
||||
// an independent copy the user can switch to explicitly via /sessions.
|
||||
// an independent copy the user can switch to explicitly via /sessions,
|
||||
// or enter from a new CLI process with the printed resume command.
|
||||
const command = forkResumeCommand(host.state.appState.workDir, forkId);
|
||||
let clipboardNote: string;
|
||||
try {
|
||||
const method = await copyTextToClipboard(command);
|
||||
// OSC 52 delivery is fire-and-forget: terminals without OSC 52 support
|
||||
// silently drop the sequence, so only native delivery may claim success
|
||||
// (same wording convention as /copy).
|
||||
clipboardNote =
|
||||
method === 'native'
|
||||
? 'Command copied to clipboard'
|
||||
: 'Command copied via terminal escape sequence (unverified)';
|
||||
} catch {
|
||||
clipboardNote = 'Failed to copy command to clipboard';
|
||||
}
|
||||
host.showStatus(
|
||||
`Session forked (${forkId}). Still in the original session; switch to the fork via /sessions.`,
|
||||
`Session forked (${forkId}). Still in the original session; switch to the fork via /sessions.\n` +
|
||||
` To enter the fork in a new process, run: ${command}\n` +
|
||||
` ${clipboardNote}`,
|
||||
);
|
||||
} catch (error) {
|
||||
const msg = formatErrorMessage(error);
|
||||
|
|
@ -86,6 +105,16 @@ export async function handleForkCommand(host: SlashCommandHost, args: string): P
|
|||
}
|
||||
}
|
||||
|
||||
function forkResumeCommand(workDir: string, forkId: string): string {
|
||||
const dir = quoteShellArg(workDir);
|
||||
// cmd.exe's `cd` only updates the given drive's remembered directory — a
|
||||
// terminal on a different drive stays put, and the resume then runs in the
|
||||
// wrong working directory. `pushd` switches drive + directory in both
|
||||
// cmd.exe and PowerShell (`cd /d` would break PowerShell).
|
||||
const changeDir = process.platform === 'win32' ? `pushd ${dir}` : `cd ${dir}`;
|
||||
return `${changeDir} && kimi --resume ${quoteShellArg(forkId)}`;
|
||||
}
|
||||
|
||||
function forkSourceTitle(host: SlashCommandHost, session: Session): string {
|
||||
const currentTitle = host.state.appState.sessionTitle?.trim();
|
||||
if (currentTitle !== undefined && currentTitle.length > 0) return currentTitle;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import {
|
|||
import { KimiTUI, type KimiTUIStartupInput, type TUIState } from '#/tui/kimi-tui';
|
||||
import type { StreamingUIController } from '#/tui/controllers/streaming-ui';
|
||||
import { handleFeedbackCommand } from '#/tui/commands/info';
|
||||
import { copyTextToClipboard } from '#/utils/clipboard/clipboard-text';
|
||||
import { openUrl } from '#/utils/open-url';
|
||||
import { createFeedbackArchivePath } from '../../src/feedback/archive';
|
||||
import { packageCodebase, scanCodebase } from '../../src/feedback/codebase';
|
||||
|
|
@ -97,6 +98,12 @@ vi.mock('../../src/feedback/archive', async (importOriginal) => {
|
|||
// out so the test suite never spawns a browser window.
|
||||
vi.mock('#/utils/open-url', () => ({ openUrl: vi.fn() }));
|
||||
|
||||
// Clipboard access spawns platform tools (pbcopy/wl-copy …) and emits OSC 52 —
|
||||
// stub it out so the suite never touches the real clipboard or stdout.
|
||||
vi.mock('#/utils/clipboard/clipboard-text', () => ({
|
||||
copyTextToClipboard: vi.fn(async () => 'native'),
|
||||
}));
|
||||
|
||||
const ESC = String.fromCodePoint(0x1b);
|
||||
const BEL = String.fromCodePoint(0x07);
|
||||
|
||||
|
|
@ -6381,6 +6388,14 @@ command = "vim"
|
|||
'Session forked (ses-fork). Still in the original session; switch to the fork via /sessions.',
|
||||
);
|
||||
});
|
||||
expect(copyTextToClipboard).toHaveBeenCalledWith(
|
||||
"cd '/tmp/proj-a' && kimi --resume 'ses-fork'",
|
||||
);
|
||||
const transcript = driver.state.transcriptContainer.render(120).join('\n');
|
||||
expect(transcript).toContain(
|
||||
"To enter the fork in a new process, run: cd '/tmp/proj-a' && kimi --resume 'ses-fork'",
|
||||
);
|
||||
expect(transcript).toContain('Command copied to clipboard');
|
||||
expect(driver.getCurrentSessionId()).toBe('ses-source');
|
||||
expect(source.close).not.toHaveBeenCalled();
|
||||
expect(forked.close).toHaveBeenCalledOnce();
|
||||
|
|
@ -6393,6 +6408,70 @@ command = "vim"
|
|||
}
|
||||
});
|
||||
|
||||
it('still prints the fork resume command when the clipboard copy fails', async () => {
|
||||
vi.mocked(copyTextToClipboard).mockRejectedValueOnce(new Error('no clipboard'));
|
||||
const source = makeSession({ id: 'ses-source' });
|
||||
const forked = makeSession({ id: 'ses-fork' });
|
||||
const forkSession = vi.fn(async () => forked);
|
||||
const { driver } = await makeDriver(source, { forkSession });
|
||||
|
||||
driver.handleUserInput('/fork');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const transcript = driver.state.transcriptContainer.render(120).join('\n');
|
||||
expect(transcript).toContain(
|
||||
"To enter the fork in a new process, run: cd '/tmp/proj-a' && kimi --resume 'ses-fork'",
|
||||
);
|
||||
expect(transcript).toContain('Failed to copy command to clipboard');
|
||||
});
|
||||
expect(driver.getCurrentSessionId()).toBe('ses-source');
|
||||
});
|
||||
|
||||
it('labels OSC 52 clipboard delivery as unverified after a fork', async () => {
|
||||
vi.mocked(copyTextToClipboard).mockResolvedValueOnce('osc52');
|
||||
const source = makeSession({ id: 'ses-source' });
|
||||
const forked = makeSession({ id: 'ses-fork' });
|
||||
const forkSession = vi.fn(async () => forked);
|
||||
const { driver } = await makeDriver(source, { forkSession });
|
||||
|
||||
driver.handleUserInput('/fork');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(driver.state.transcriptContainer.render(120).join('\n')).toContain(
|
||||
'Command copied via terminal escape sequence (unverified)',
|
||||
);
|
||||
});
|
||||
expect(driver.getCurrentSessionId()).toBe('ses-source');
|
||||
});
|
||||
|
||||
it('prints a pushd-based fork resume command on Windows', async () => {
|
||||
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
try {
|
||||
const source = makeSession({ id: 'ses-source' });
|
||||
const forked = makeSession({ id: 'ses-fork' });
|
||||
const forkSession = vi.fn(async () => forked);
|
||||
const { driver } = await makeDriver(source, { forkSession }, {
|
||||
...makeStartupInput(),
|
||||
workDir: 'D:\\proj',
|
||||
});
|
||||
|
||||
driver.handleUserInput('/fork');
|
||||
|
||||
// cmd.exe's `cd` does not switch drives; pushd works in cmd + PowerShell.
|
||||
await vi.waitFor(() => {
|
||||
expect(copyTextToClipboard).toHaveBeenCalledWith(
|
||||
'pushd "D:\\proj" && kimi --resume "ses-fork"',
|
||||
);
|
||||
});
|
||||
expect(driver.getCurrentSessionId()).toBe('ses-source');
|
||||
} finally {
|
||||
if (platformDescriptor !== undefined) {
|
||||
Object.defineProperty(process, 'platform', platformDescriptor);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the current session when fork fails', async () => {
|
||||
const forkSession = vi.fn(async () => {
|
||||
throw new Error('fork unavailable');
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ To explore a new direction without disrupting the current conversation, use `/fo
|
|||
|
||||
Forking does not switch you away: you stay in the original session and the conversation continues untouched. The fork is an independent copy you can switch to at any time using `/sessions`. A saved `/goal` is not copied to the fork. Start a new goal there if you want autonomous goal work.
|
||||
|
||||
After forking, the CLI prints a ready-to-run `kimi --resume` command (also copied to the clipboard) so you can enter the fork directly from a new terminal process.
|
||||
|
||||
## Exporting a session
|
||||
|
||||
Use `kimi export` to package a session as a ZIP file — useful for sharing, archiving, or filing a bug report:
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ kimi --session
|
|||
|
||||
fork 后你仍停留在原会话,对话不受影响、可以直接继续;派生出的副本与原会话彼此独立,可以随时通过 `/sessions` 切换过去。已保存的 `/goal` 不会复制到派生会话。如果你想在派生会话中进行自主 goal 工作,需要在那里开始一个新 goal。
|
||||
|
||||
fork 完成后,CLI 会打印一条可直接运行的 `kimi --resume` 命令(并自动复制到剪贴板),方便你在新终端进程中直接进入派生会话。
|
||||
|
||||
## 导出会话
|
||||
|
||||
用 `kimi export` 把会话打包为 ZIP,适合分享、归档或提交问题反馈:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue