From 952ca419775c6355c8a6ea4fd8cebc111193fc9e Mon Sep 17 00:00:00 2001 From: bj456736 Date: Sat, 15 Aug 2026 05:39:58 +0000 Subject: [PATCH] feat(tui): print and copy the resume command after /fork --- .changeset/fork-print-resume-command.md | 5 +++ apps/kimi-code/src/tui/commands/session.ts | 18 +++++++-- .../test/tui/kimi-tui-message-flow.test.ts | 37 ++++++++++++++++++- docs/en/guides/sessions.md | 2 +- docs/zh/guides/sessions.md | 2 +- 5 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 .changeset/fork-print-resume-command.md diff --git a/.changeset/fork-print-resume-command.md b/.changeset/fork-print-resume-command.md new file mode 100644 index 00000000000..1969235ee2b --- /dev/null +++ b/.changeset/fork-print-resume-command.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +`/fork` now prints a ready-to-run `kimi -r ` command for entering the fork in a new terminal and copies it to the clipboard. diff --git a/apps/kimi-code/src/tui/commands/session.ts b/apps/kimi-code/src/tui/commands/session.ts index 1a80c1947fa..ef6fd28506b 100644 --- a/apps/kimi-code/src/tui/commands/session.ts +++ b/apps/kimi-code/src/tui/commands/session.ts @@ -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'; @@ -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 ` — print the full command + // and copy it to the clipboard like the cross-cwd resume hint does. + const command = `kimi -r ${forkId}`; + 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}`); diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts index 619ecf2c2f4..4a73a5b902c 100644 --- a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -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,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); @@ -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(); @@ -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'); diff --git a/docs/en/guides/sessions.md b/docs/en/guides/sessions.md index 39aa2cb9e96..baf013e6482 100644 --- a/docs/en/guides/sessions.md +++ b/docs/en/guides/sessions.md @@ -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 ` 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 diff --git a/docs/zh/guides/sessions.md b/docs/zh/guides/sessions.md index 6501d29a16a..c0ef593f978 100644 --- a/docs/zh/guides/sessions.md +++ b/docs/zh/guides/sessions.md @@ -85,7 +85,7 @@ kimi --session /fork ``` -fork 后你仍停留在原会话,对话不受影响、可以直接继续;派生出的副本与原会话彼此独立,可以随时通过 `/sessions` 切换过去。已保存的 `/goal` 不会复制到派生会话。如果你想在派生会话中进行自主 goal 工作,需要在那里开始一个新 goal。 +fork 后你仍停留在原会话,对话不受影响、可以直接继续;派生出的副本与原会话彼此独立,可以随时通过 `/sessions` 切换过去。同时会打印一条完整的 `kimi -r <会话 ID>` 命令并自动复制到剪贴板,方便在新终端里直接进入派生会话。已保存的 `/goal` 不会复制到派生会话。如果你想在派生会话中进行自主 goal 工作,需要在那里开始一个新 goal。 ## 导出会话