From 3dd12fa5c7433b5d9e6cbf8f3494a80e5466b92e Mon Sep 17 00:00:00 2001 From: liruifengv Date: Wed, 27 May 2026 18:44:04 +0800 Subject: [PATCH 1/4] feat(tui): add /export-md slash command Add a new /export-md (alias: /export) command that exports the current session conversation as a human-readable Markdown file. The export includes YAML frontmatter metadata, an overview section, and turn-by-turn dialogue with collapsible thinking blocks and tool call/result details. Also exposes Session.getContext() on the SDK to allow TUI-layer access to the agent's conversation history. --- apps/kimi-code/src/tui/commands/registry.ts | 6 + apps/kimi-code/src/tui/kimi-tui.ts | 51 +++ .../src/tui/utils/export-markdown.ts | 241 +++++++++++ .../test/tui/export-markdown.test.ts | 377 ++++++++++++++++++ packages/node-sdk/src/rpc.ts | 9 + packages/node-sdk/src/session.ts | 7 +- 6 files changed, 690 insertions(+), 1 deletion(-) create mode 100644 apps/kimi-code/src/tui/utils/export-markdown.ts create mode 100644 apps/kimi-code/test/tui/export-markdown.test.ts diff --git a/apps/kimi-code/src/tui/commands/registry.ts b/apps/kimi-code/src/tui/commands/registry.ts index 8184ea5216..6313b2f0d3 100644 --- a/apps/kimi-code/src/tui/commands/registry.ts +++ b/apps/kimi-code/src/tui/commands/registry.ts @@ -145,6 +145,12 @@ export const BUILTIN_SLASH_COMMANDS = [ description: 'Connect a provider from a model catalog', priority: 40, }, + { + name: 'export-md', + aliases: ['export'], + description: 'Export current session as a Markdown file', + priority: 40, + }, { name: 'export-debug-zip', aliases: [], diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 56bd5e2182..9a4175169a 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -1590,6 +1590,9 @@ export class KimiTUI { case 'fork': await this.handleForkCommand(args); return; + case 'export-md': + await this.handleExportMdCommand(args); + return; case 'export-debug-zip': await this.handleExportDebugZipCommand(); return; @@ -5531,6 +5534,54 @@ export class KimiTUI { } } + private async handleExportMdCommand(args: string): Promise { + const session = this.session; + if (session === undefined) { + this.showError(NO_ACTIVE_SESSION_MESSAGE); + return; + } + + this.showStatus('Exporting session as Markdown…'); + try { + const context = await session.getContext(); + if (context.history.length === 0) { + this.showError('No messages to export.'); + return; + } + + const { buildExportMarkdown } = await import('./utils/export-markdown'); + const { writeFile, mkdir } = await import('node:fs/promises'); + const { resolve, dirname } = await import('node:path'); + + const now = new Date(); + const shortId = session.id.slice(0, 8); + const timestamp = now.toISOString().replaceAll(/[-:]/g, '').replace(/T/, '-').slice(0, 15); + const defaultName = `kimi-export-${shortId}-${timestamp}.md`; + + const trimmedArgs = args.trim(); + const outputPath = trimmedArgs.length > 0 + ? resolve(trimmedArgs) + : resolve(this.state.appState.workDir, defaultName); + + const md = buildExportMarkdown({ + sessionId: session.id, + workDir: this.state.appState.workDir, + history: context.history, + tokenCount: context.tokenCount, + now, + }); + + await mkdir(dirname(outputPath), { recursive: true }); + await writeFile(outputPath, md, 'utf-8'); + + const linked = toTerminalHyperlink(outputPath, pathToFileURL(outputPath).href); + this.showNotice(`Exported ${String(context.history.length)} messages`, linked); + } catch (error) { + const msg = formatErrorMessage(error); + this.showError(`Failed to export session: ${msg}`); + } + } + private async handleExportDebugZipCommand(): Promise { const session = this.session; if (session === undefined) { diff --git a/apps/kimi-code/src/tui/utils/export-markdown.ts b/apps/kimi-code/src/tui/utils/export-markdown.ts new file mode 100644 index 0000000000..6eac053227 --- /dev/null +++ b/apps/kimi-code/src/tui/utils/export-markdown.ts @@ -0,0 +1,241 @@ +import type { ContentPart, ContextMessage, PromptOrigin, ToolCall } from '@moonshot-ai/kimi-code-sdk'; + +const HINT_KEYS = ['path', 'file_path', 'command', 'query', 'url', 'name', 'pattern'] as const; + +const MAX_HINT_WIDTH = 60; + +export function extractToolCallHint(argsJson: string): string { + let parsed: unknown; + try { + parsed = JSON.parse(argsJson); + } catch { + return ''; + } + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return ''; + + const args = parsed as Record; + + for (const key of HINT_KEYS) { + const val = args[key]; + if (typeof val === 'string' && val.trim().length > 0) { + return shorten(val, MAX_HINT_WIDTH); + } + } + + for (const val of Object.values(args)) { + if (typeof val === 'string' && val.length > 0 && val.length <= 80) { + return shorten(val, MAX_HINT_WIDTH); + } + } + + return ''; +} + +function shorten(text: string, width: number): string { + if (text.length <= width) return text; + return `${text.slice(0, width)}…`; +} + +export function formatContentPartMd(part: ContentPart): string { + switch (part.type) { + case 'text': + return part.text; + case 'think': + if (!part.think.trim()) return ''; + return `
Thinking\n\n${part.think}\n\n
`; + case 'image_url': + return '[image]'; + case 'audio_url': + return '[audio]'; + case 'video_url': + return '[video]'; + default: + return `[${(part as ContentPart).type}]`; + } +} + +export function formatToolCallMd(tc: ToolCall): string { + const argsRaw = tc.arguments ?? '{}'; + const hint = extractToolCallHint(argsRaw); + let title = `#### Tool Call: ${tc.name}`; + if (hint) { + title += ` (\`${hint}\`)`; + } + + let argsFormatted: string; + try { + argsFormatted = JSON.stringify(JSON.parse(argsRaw), null, 2); + } catch { + argsFormatted = argsRaw; + } + + return `${title}\n\n\`\`\`json\n${argsFormatted}\n\`\`\``; +} + +function formatToolResultMd(msg: ContextMessage, toolName: string, hint: string): string { + const callId = msg.toolCallId ?? 'unknown'; + const parts: string[] = []; + for (const part of msg.content) { + const text = formatContentPartMd(part); + if (text.trim()) parts.push(text); + } + const resultText = parts.join('\n'); + + let summary = `Tool Result: ${toolName}`; + if (hint) summary += ` (\`${hint}\`)`; + + return ( + `
${summary}\n\n` + + `\n` + + `${resultText}\n\n` + + '
' + ); +} + +const INTERNAL_ORIGINS = new Set([ + 'injection', + 'system_trigger', + 'compaction_summary', + 'hook_result', +]); + +export function isInternalMessage(msg: ContextMessage): boolean { + const origin = msg.origin; + if (origin === undefined) return false; + return INTERNAL_ORIGINS.has(origin.kind); +} + +export function groupIntoTurns(history: readonly ContextMessage[]): ContextMessage[][] { + const turns: ContextMessage[][] = []; + let current: ContextMessage[] = []; + + for (const msg of history) { + if (isInternalMessage(msg)) continue; + if (msg.role === 'user' && current.length > 0) { + turns.push(current); + current = []; + } + current.push(msg); + } + + if (current.length > 0) turns.push(current); + return turns; +} + +function formatTurnMd(messages: readonly ContextMessage[], turnNumber: number): string { + const lines: string[] = [`## Turn ${String(turnNumber)}`, '']; + + const toolCallInfo = new Map(); + let assistantHeaderWritten = false; + + for (const msg of messages) { + if (isInternalMessage(msg)) continue; + + if (msg.role === 'user') { + lines.push('### User', ''); + for (const part of msg.content) { + const text = formatContentPartMd(part); + if (text.trim()) { + lines.push(text, ''); + } + } + } else if (msg.role === 'assistant') { + if (!assistantHeaderWritten) { + lines.push('### Assistant', ''); + assistantHeaderWritten = true; + } + + for (const part of msg.content) { + const text = formatContentPartMd(part); + if (text.trim()) { + lines.push(text, ''); + } + } + + for (const tc of msg.toolCalls) { + const hint = extractToolCallHint(tc.arguments ?? '{}'); + toolCallInfo.set(tc.id, { name: tc.name, hint }); + lines.push(formatToolCallMd(tc), ''); + } + } else if (msg.role === 'tool') { + const tcId = msg.toolCallId ?? ''; + const info = toolCallInfo.get(tcId) ?? { name: 'unknown', hint: '' }; + lines.push(formatToolResultMd(msg, info.name, info.hint), ''); + } else if (msg.role === 'system') { + lines.push(`### ${msg.role.charAt(0).toUpperCase()}${msg.role.slice(1)}`, ''); + for (const part of msg.content) { + const text = formatContentPartMd(part); + if (text.trim()) { + lines.push(text, ''); + } + } + } + } + + return lines.join('\n'); +} + +function buildOverview( + history: readonly ContextMessage[], + turns: readonly ContextMessage[][], +): string { + let topic = ''; + for (const msg of history) { + if (msg.role === 'user' && !isInternalMessage(msg)) { + const textParts = msg.content + .filter((p): p is { type: 'text'; text: string } => p.type === 'text') + .map((p) => p.text); + topic = shorten(textParts.join(' '), 80); + break; + } + } + + const toolCallCount = history.reduce( + (sum, msg) => sum + msg.toolCalls.length, + 0, + ); + + return [ + '## Overview', + '', + topic ? `- **Topic**: ${topic}` : '- **Topic**: (empty)', + `- **Conversation**: ${String(turns.length)} turns | ${String(toolCallCount)} tool calls`, + '', + '---', + ].join('\n'); +} + +export interface BuildExportMarkdownInput { + readonly sessionId: string; + readonly workDir: string; + readonly history: readonly ContextMessage[]; + readonly tokenCount: number; + readonly now: Date; +} + +export function buildExportMarkdown(input: BuildExportMarkdownInput): string { + const { sessionId, workDir, history, tokenCount, now } = input; + + const lines: string[] = [ + '---', + `session_id: ${sessionId}`, + `exported_at: ${now.toISOString()}`, + `work_dir: ${workDir}`, + `message_count: ${String(history.length)}`, + `token_count: ${String(tokenCount)}`, + '---', + '', + '# Kimi Session Export', + '', + ]; + + const turns = groupIntoTurns(history); + lines.push(buildOverview(history, turns)); + lines.push(''); + + for (let i = 0; i < turns.length; i++) { + lines.push(formatTurnMd(turns[i]!, i + 1)); + } + + return lines.join('\n'); +} diff --git a/apps/kimi-code/test/tui/export-markdown.test.ts b/apps/kimi-code/test/tui/export-markdown.test.ts new file mode 100644 index 0000000000..1c59cd6e67 --- /dev/null +++ b/apps/kimi-code/test/tui/export-markdown.test.ts @@ -0,0 +1,377 @@ +import { describe, expect, it } from 'vitest'; +import type { ContentPart, ToolCall } from '@moonshot-ai/kimi-code-sdk'; +import type { ContextMessage, PromptOrigin } from '@moonshot-ai/kimi-code-sdk'; + +import { + buildExportMarkdown, + extractToolCallHint, + formatContentPartMd, + formatToolCallMd, + groupIntoTurns, + isInternalMessage, +} from '../../src/tui/utils/export-markdown'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function userMsg(text: string, origin?: PromptOrigin): ContextMessage { + return { + role: 'user', + content: [{ type: 'text', text }], + toolCalls: [], + origin, + }; +} + +function assistantMsg( + text: string, + toolCalls: ToolCall[] = [], + thinkText?: string, +): ContextMessage { + const content: ContentPart[] = []; + if (thinkText !== undefined) { + content.push({ type: 'think', think: thinkText }); + } + content.push({ type: 'text', text }); + return { + role: 'assistant', + content, + toolCalls, + }; +} + +function toolMsg(callId: string, text: string): ContextMessage { + return { + role: 'tool', + content: [{ type: 'text', text }], + toolCalls: [], + toolCallId: callId, + }; +} + +function makeToolCall(id: string, name: string, args: Record): ToolCall { + return { + type: 'function', + id, + name, + arguments: JSON.stringify(args), + }; +} + +// --------------------------------------------------------------------------- +// extractToolCallHint +// --------------------------------------------------------------------------- + +describe('extractToolCallHint', () => { + it('extracts path from arguments', () => { + expect(extractToolCallHint(JSON.stringify({ path: '/foo/bar.ts' }))).toBe('/foo/bar.ts'); + }); + + it('extracts command from arguments', () => { + expect(extractToolCallHint(JSON.stringify({ command: 'ls -la' }))).toBe('ls -la'); + }); + + it('prefers path over command', () => { + expect(extractToolCallHint(JSON.stringify({ command: 'ls', path: '/a.ts' }))).toBe('/a.ts'); + }); + + it('falls back to first short string value', () => { + expect(extractToolCallHint(JSON.stringify({ foo: 'hello world' }))).toBe('hello world'); + }); + + it('returns empty string for invalid JSON', () => { + expect(extractToolCallHint('not json')).toBe(''); + }); + + it('returns empty string for non-object JSON', () => { + expect(extractToolCallHint('"just a string"')).toBe(''); + }); + + it('truncates long values', () => { + const long = 'a'.repeat(100); + const hint = extractToolCallHint(JSON.stringify({ path: long })); + expect(hint.length).toBeLessThanOrEqual(63); // 60 + "…" + }); +}); + +// --------------------------------------------------------------------------- +// formatContentPartMd +// --------------------------------------------------------------------------- + +describe('formatContentPartMd', () => { + it('renders text part', () => { + expect(formatContentPartMd({ type: 'text', text: 'hello' })).toBe('hello'); + }); + + it('renders think part as collapsible', () => { + const result = formatContentPartMd({ type: 'think', think: 'reasoning here' }); + expect(result).toContain('
'); + expect(result).toContain('Thinking'); + expect(result).toContain('reasoning here'); + }); + + it('returns empty for blank think', () => { + expect(formatContentPartMd({ type: 'think', think: ' ' })).toBe(''); + }); + + it('renders image placeholder', () => { + expect( + formatContentPartMd({ type: 'image_url', imageUrl: { url: 'http://x' } }), + ).toBe('[image]'); + }); + + it('renders audio placeholder', () => { + expect( + formatContentPartMd({ type: 'audio_url', audioUrl: { url: 'http://x' } }), + ).toBe('[audio]'); + }); + + it('renders video placeholder', () => { + expect( + formatContentPartMd({ type: 'video_url', videoUrl: { url: 'http://x' } }), + ).toBe('[video]'); + }); +}); + +// --------------------------------------------------------------------------- +// formatToolCallMd +// --------------------------------------------------------------------------- + +describe('formatToolCallMd', () => { + it('renders tool call with hint', () => { + const tc = makeToolCall('c1', 'Bash', { command: 'ls' }); + const md = formatToolCallMd(tc); + expect(md).toContain('#### Tool Call: Bash'); + expect(md).toContain('`ls`'); + expect(md).toContain('```json'); + expect(md).toContain('"command": "ls"'); + }); + + it('renders tool call without hint', () => { + const tc = makeToolCall('c1', 'CustomTool', {}); + const md = formatToolCallMd(tc); + expect(md).toContain('#### Tool Call: CustomTool'); + expect(md).not.toContain('(`'); + }); +}); + +// --------------------------------------------------------------------------- +// isInternalMessage +// --------------------------------------------------------------------------- + +describe('isInternalMessage', () => { + it('marks injection origin as internal', () => { + expect(isInternalMessage(userMsg('x', { kind: 'injection', variant: 'test' }))).toBe(true); + }); + + it('marks system_trigger origin as internal', () => { + expect( + isInternalMessage(userMsg('x', { kind: 'system_trigger', name: 'test' })), + ).toBe(true); + }); + + it('marks compaction_summary origin as internal', () => { + expect(isInternalMessage(userMsg('x', { kind: 'compaction_summary' }))).toBe(true); + }); + + it('marks hook_result origin as internal', () => { + expect( + isInternalMessage(userMsg('x', { kind: 'hook_result', event: 'test' })), + ).toBe(true); + }); + + it('keeps real user messages', () => { + expect(isInternalMessage(userMsg('hello', { kind: 'user' }))).toBe(false); + }); + + it('keeps assistant messages', () => { + expect(isInternalMessage(assistantMsg('hi'))).toBe(false); + }); + + it('keeps tool messages', () => { + expect(isInternalMessage(toolMsg('c1', 'output'))).toBe(false); + }); + + it('keeps user messages without origin', () => { + expect(isInternalMessage(userMsg('hello'))).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// groupIntoTurns +// --------------------------------------------------------------------------- + +describe('groupIntoTurns', () => { + it('groups messages into turns starting at user messages', () => { + const msgs: ContextMessage[] = [ + userMsg('q1', { kind: 'user' }), + assistantMsg('a1'), + userMsg('q2', { kind: 'user' }), + assistantMsg('a2'), + ]; + const turns = groupIntoTurns(msgs); + expect(turns).toHaveLength(2); + expect(turns[0]).toHaveLength(2); + expect(turns[1]).toHaveLength(2); + }); + + it('skips internal messages', () => { + const msgs: ContextMessage[] = [ + userMsg('q1', { kind: 'user' }), + userMsg('injected', { kind: 'injection', variant: 'test' }), + assistantMsg('a1'), + ]; + const turns = groupIntoTurns(msgs); + expect(turns).toHaveLength(1); + expect(turns[0]).toHaveLength(2); + }); + + it('returns empty for empty input', () => { + expect(groupIntoTurns([])).toHaveLength(0); + }); + + it('handles tool messages within a turn', () => { + const tc = makeToolCall('c1', 'Bash', { command: 'ls' }); + const msgs: ContextMessage[] = [ + userMsg('do it', { kind: 'user' }), + assistantMsg('ok', [tc]), + toolMsg('c1', 'file1.txt'), + assistantMsg('done'), + ]; + const turns = groupIntoTurns(msgs); + expect(turns).toHaveLength(1); + expect(turns[0]).toHaveLength(4); + }); +}); + +// --------------------------------------------------------------------------- +// buildExportMarkdown +// --------------------------------------------------------------------------- + +describe('buildExportMarkdown', () => { + const now = new Date('2026-05-27T10:00:00+08:00'); + + it('builds complete markdown with frontmatter and overview', () => { + const msgs: ContextMessage[] = [ + userMsg('Hello world', { kind: 'user' }), + assistantMsg('Hi there'), + ]; + const md = buildExportMarkdown({ + sessionId: 'ses_abc12345xyz', + workDir: '/home/user/project', + history: msgs, + tokenCount: 1234, + now, + }); + + expect(md).toContain('---'); + expect(md).toContain('session_id: ses_abc12345xyz'); + expect(md).toContain('work_dir: /home/user/project'); + expect(md).toContain('message_count: 2'); + expect(md).toContain('token_count: 1234'); + expect(md).toContain('# Kimi Session Export'); + expect(md).toContain('## Overview'); + expect(md).toContain('Hello world'); + expect(md).toContain('## Turn 1'); + expect(md).toContain('### User'); + expect(md).toContain('### Assistant'); + expect(md).toContain('Hi there'); + }); + + it('includes thinking in collapsible details', () => { + const msgs: ContextMessage[] = [ + userMsg('question', { kind: 'user' }), + assistantMsg('answer', [], 'deep thought'), + ]; + const md = buildExportMarkdown({ + sessionId: 'ses_test', + workDir: '/tmp', + history: msgs, + tokenCount: 0, + now, + }); + expect(md).toContain('
Thinking'); + expect(md).toContain('deep thought'); + }); + + it('renders tool calls and results', () => { + const tc = makeToolCall('c1', 'Read', { file_path: '/foo.ts' }); + const msgs: ContextMessage[] = [ + userMsg('read file', { kind: 'user' }), + assistantMsg('let me read', [tc]), + toolMsg('c1', 'file contents here'), + assistantMsg('the file contains...'), + ]; + const md = buildExportMarkdown({ + sessionId: 'ses_test', + workDir: '/tmp', + history: msgs, + tokenCount: 0, + now, + }); + expect(md).toContain('#### Tool Call: Read'); + expect(md).toContain('`/foo.ts`'); + expect(md).toContain('Tool Result: Read'); + expect(md).toContain('file contents here'); + }); + + it('filters out internal messages', () => { + const msgs: ContextMessage[] = [ + userMsg('hello', { kind: 'user' }), + userMsg('injected stuff', { kind: 'injection', variant: 'system-reminder' }), + assistantMsg('response'), + ]; + const md = buildExportMarkdown({ + sessionId: 'ses_test', + workDir: '/tmp', + history: msgs, + tokenCount: 0, + now, + }); + expect(md).not.toContain('injected stuff'); + expect(md).toContain('hello'); + expect(md).toContain('response'); + }); + + it('counts turns correctly in overview', () => { + const msgs: ContextMessage[] = [ + userMsg('q1', { kind: 'user' }), + assistantMsg('a1'), + userMsg('q2', { kind: 'user' }), + assistantMsg('a2'), + userMsg('q3', { kind: 'user' }), + assistantMsg('a3'), + ]; + const md = buildExportMarkdown({ + sessionId: 'ses_test', + workDir: '/tmp', + history: msgs, + tokenCount: 500, + now, + }); + expect(md).toContain('3 turns'); + expect(md).toContain('## Turn 1'); + expect(md).toContain('## Turn 2'); + expect(md).toContain('## Turn 3'); + }); + + it('counts tool calls in overview', () => { + const tc1 = makeToolCall('c1', 'Bash', { command: 'ls' }); + const tc2 = makeToolCall('c2', 'Read', { file_path: '/a.ts' }); + const msgs: ContextMessage[] = [ + userMsg('do things', { kind: 'user' }), + assistantMsg('ok', [tc1, tc2]), + toolMsg('c1', 'out1'), + toolMsg('c2', 'out2'), + ]; + const md = buildExportMarkdown({ + sessionId: 'ses_test', + workDir: '/tmp', + history: msgs, + tokenCount: 0, + now, + }); + expect(md).toContain('2 tool calls'); + }); +}); diff --git a/packages/node-sdk/src/rpc.ts b/packages/node-sdk/src/rpc.ts index 05bc7dfd0b..97fb116161 100644 --- a/packages/node-sdk/src/rpc.ts +++ b/packages/node-sdk/src/rpc.ts @@ -4,6 +4,7 @@ import { KimiCore, makeErrorPayload, resolveKimiHome, + type AgentContextData, type ApprovalRequest, type ApprovalResponse, type CoreAPI, @@ -308,6 +309,14 @@ export class SDKRpcClient { }); } + async getContext(input: SessionIdRpcInput): Promise { + const rpc = await this.getRpc(); + return rpc.getContext({ + sessionId: input.sessionId, + agentId: this.interactiveAgentId, + }); + } + async getUsage(input: SessionIdRpcInput): Promise { const rpc = await this.getRpc(); return rpc.getUsage({ diff --git a/packages/node-sdk/src/session.ts b/packages/node-sdk/src/session.ts index 7728dfe2e1..55f2989f2c 100644 --- a/packages/node-sdk/src/session.ts +++ b/packages/node-sdk/src/session.ts @@ -1,4 +1,4 @@ -import { ErrorCodes, KimiError, type KimiErrorCode } from '@moonshot-ai/agent-core'; +import { ErrorCodes, KimiError, type AgentContextData, type KimiErrorCode } from '@moonshot-ai/agent-core'; import { type ApprovalHandler, type Event, type QuestionHandler } from '#/events'; import type { SDKRpcClient } from '#/rpc'; import type { @@ -163,6 +163,11 @@ export class Session { await this.rpc.cancelCompaction({ sessionId: this.id }); } + async getContext(): Promise { + this.ensureOpen(); + return this.rpc.getContext({ sessionId: this.id }); + } + async getUsage(): Promise { this.ensureOpen(); return this.rpc.getUsage({ sessionId: this.id }); From 1d49287a5b46acb8608ec02c18d1be93fd7210c7 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Wed, 27 May 2026 18:44:34 +0800 Subject: [PATCH 2/4] chore: add changeset for /export-md --- .changeset/tui-export-md.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/tui-export-md.md diff --git a/.changeset/tui-export-md.md b/.changeset/tui-export-md.md new file mode 100644 index 0000000000..475c66f493 --- /dev/null +++ b/.changeset/tui-export-md.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/kimi-code": minor +"@moonshot-ai/kimi-code-sdk": minor +--- + +Add `/export-md` slash command to export the current session as a human-readable Markdown file. Also exposes `Session.getContext()` on the SDK. From 6b9d82bf00f806e8f9e7eceb2f90b6dd86f0626c Mon Sep 17 00:00:00 2001 From: liruifengv Date: Wed, 27 May 2026 18:47:09 +0800 Subject: [PATCH 3/4] fix: use static imports instead of dynamic imports in handleExportMdCommand --- apps/kimi-code/src/tui/kimi-tui.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 9a4175169a..03df4fbaad 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -8,8 +8,9 @@ */ import { writeFileSync } from 'node:fs'; +import { mkdir, writeFile } from 'node:fs/promises'; import { release as osRelease, type as osType } from 'node:os'; -import { join } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { @@ -111,6 +112,7 @@ import { parseImageMeta } from '#/utils/image/image-mime'; import { getInputHistoryFile } from '#/utils/paths'; import { editInExternalEditor, resolveEditorCommand } from '#/utils/process/external-editor'; import { detectFdPath } from '#/utils/process/fd-detect'; +import { buildExportMarkdown } from './utils/export-markdown'; import { BUILTIN_SLASH_COMMANDS, @@ -5549,10 +5551,6 @@ export class KimiTUI { return; } - const { buildExportMarkdown } = await import('./utils/export-markdown'); - const { writeFile, mkdir } = await import('node:fs/promises'); - const { resolve, dirname } = await import('node:path'); - const now = new Date(); const shortId = session.id.slice(0, 8); const timestamp = now.toISOString().replaceAll(/[-:]/g, '').replace(/T/, '-').slice(0, 15); From bd4c348e9acd6d14679643d4e8120f4110370a76 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Wed, 27 May 2026 18:49:47 +0800 Subject: [PATCH 4/4] chore: simplify changeset wording --- .changeset/tui-export-md.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tui-export-md.md b/.changeset/tui-export-md.md index 475c66f493..375cfdbdc3 100644 --- a/.changeset/tui-export-md.md +++ b/.changeset/tui-export-md.md @@ -3,4 +3,4 @@ "@moonshot-ai/kimi-code-sdk": minor --- -Add `/export-md` slash command to export the current session as a human-readable Markdown file. Also exposes `Session.getContext()` on the SDK. +Add `/export-md` slash command to export the current session as a Markdown file.