Skip to content
Closed
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/fork-print-resume-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

`/fork` now prints a ready-to-run `kimi -r <session id>` command for entering the fork in a new terminal and copies it to the clipboard.
18 changes: 14 additions & 4 deletions apps/kimi-code/src/tui/commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 { toTerminalHyperlink } from '#/utils/terminal-hyperlink';
import { LLM_NOT_SET_MESSAGE, NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui';
Expand Down Expand Up @@ -76,10 +77,19 @@ 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.
host.showStatus(
`Session forked (${forkId}). Still in the original session; switch to the fork via /sessions.`,
);
// an independent copy the user can switch to explicitly via /sessions,
// or enter in a new terminal with `kimi -r <id>` — print the full command
// and copy it to the clipboard like the cross-cwd resume hint does.
const command = `kimi -r ${forkId}`;

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 Include the fork working directory in the resume command

When the copied command is run from a new terminal whose current directory differs from the source session's working directory, it does not enter the fork: the interactive startup path in apps/kimi-code/src/tui/kimi-tui.ts rejects such resumes at lines 882–893. Build the command with the source working directory and shell quoting, as showResumeOtherWorkDirHint already does, so the advertised ready-to-run command works regardless of where the new terminal starts.

Useful? React with 👍 / 👎.

const message =
`Session forked (${forkId}). Still in the original session; switch to the fork via /sessions.\n` +
` To enter the fork in a new terminal, run: ${command}`;
try {
await copyTextToClipboard(command);
host.showStatus(`${message}\n Command copied to clipboard`);
} catch {
host.showStatus(message);
}
} catch (error) {
const msg = formatErrorMessage(error);
host.showError(`Failed to fork session: ${msg}`);
Expand Down
37 changes: 36 additions & 1 deletion apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -97,6 +98,14 @@ 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() }));

// /fork copies the resume command to the clipboard — stub it so tests never
// touch a real clipboard and can assert the copied text.
vi.mock('#/utils/clipboard/clipboard-text', () => ({
copyTextToClipboard: vi.fn(async () => {}),
}));

const copyTextToClipboardMock = vi.mocked(copyTextToClipboard);

const ESC = String.fromCodePoint(0x1b);
const BEL = String.fromCodePoint(0x07);

Expand Down Expand Up @@ -6377,10 +6386,14 @@ command = "vim"
id: 'ses-source',
title: 'Fork: Source title',
});
expect(driver.state.transcriptContainer.render(120).join('\n')).toContain(
const transcript = driver.state.transcriptContainer.render(120).join('\n');
expect(transcript).toContain(
'Session forked (ses-fork). Still in the original session; switch to the fork via /sessions.',
);
expect(transcript).toContain('To enter the fork in a new terminal, run: kimi -r ses-fork');
expect(transcript).toContain('Command copied to clipboard');
});
expect(copyTextToClipboardMock).toHaveBeenCalledWith('kimi -r ses-fork');
expect(driver.getCurrentSessionId()).toBe('ses-source');
expect(source.close).not.toHaveBeenCalled();
expect(forked.close).toHaveBeenCalledOnce();
Expand All @@ -6393,6 +6406,28 @@ command = "vim"
}
});

it('still prints the fork resume command when the clipboard is unavailable', async () => {
copyTextToClipboardMock.mockRejectedValueOnce(new Error('no clipboard'));
const forked = makeSession({ id: 'ses-fork' });
const forkSession = vi.fn(async () => forked);
const { driver } = await makeDriver(makeSession({ id: 'ses-source' }), { forkSession });

driver.handleUserInput('/fork');

await vi.waitFor(() => {
const transcript = driver.state.transcriptContainer.render(120).join('\n');
expect(transcript).toContain(
'Session forked (ses-fork). Still in the original session; switch to the fork via /sessions.',
);
expect(transcript).toContain('To enter the fork in a new terminal, run: kimi -r ses-fork');
});
expect(copyTextToClipboardMock).toHaveBeenCalledWith('kimi -r ses-fork');
expect(driver.state.transcriptContainer.render(120).join('\n')).not.toContain(
'Command copied to clipboard',
);
expect(driver.getCurrentSessionId()).toBe('ses-source');
});

it('keeps the current session when fork fails', async () => {
const forkSession = vi.fn(async () => {
throw new Error('fork unavailable');
Expand Down
2 changes: 1 addition & 1 deletion docs/en/guides/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ To explore a new direction without disrupting the current conversation, use `/fo
/fork
```

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.
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 ready-to-run `kimi -r <session id>` command is printed and copied to the clipboard, so you can jump straight into the fork from a new terminal. A saved `/goal` is not copied to the fork. Start a new goal there if you want autonomous goal work.

## Exporting a session

Expand Down
2 changes: 1 addition & 1 deletion docs/zh/guides/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ kimi --session
/fork
```

fork 后你仍停留在原会话,对话不受影响、可以直接继续;派生出的副本与原会话彼此独立,可以随时通过 `/sessions` 切换过去。已保存的 `/goal` 不会复制到派生会话。如果你想在派生会话中进行自主 goal 工作,需要在那里开始一个新 goal。
fork 后你仍停留在原会话,对话不受影响、可以直接继续;派生出的副本与原会话彼此独立,可以随时通过 `/sessions` 切换过去。同时会打印一条完整的 `kimi -r <会话 ID>` 命令并自动复制到剪贴板,方便在新终端里直接进入派生会话。已保存的 `/goal` 不会复制到派生会话。如果你想在派生会话中进行自主 goal 工作,需要在那里开始一个新 goal。

## 导出会话

Expand Down
Loading