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/open-empty-btw-panel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Allow `/btw` to open the side-channel panel before entering a question.
5 changes: 0 additions & 5 deletions apps/kimi-code/src/tui/commands/btw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ import type { SlashCommandHost } from './dispatch';

export async function handleBtwCommand(host: SlashCommandHost, args: string): Promise<void> {
const prompt = args.trim();
if (prompt.length === 0) {
host.showError('Usage: /btw <question>');
return;
}

const session = host.session;
if (host.state.appState.model.trim().length === 0 || session === undefined) {
host.showError(LLM_NOT_SET_MESSAGE);
Expand Down
4 changes: 4 additions & 0 deletions apps/kimi-code/src/tui/components/panes/btw-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ export class BtwPanelComponent implements Component {
return this.currentTurn()?.phase === 'running';
}

isEmpty(): boolean {
return this.turns.length === 0;
}

scroll(direction: 'up' | 'down'): boolean {
if (this.maxScrollTop <= 0) return false;
const current = this.followTail ? this.maxScrollTop : this.scrollTop;
Expand Down
10 changes: 7 additions & 3 deletions apps/kimi-code/src/tui/controllers/btw-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class BtwPanelController {

clear(): void {
const active = this.active;
if (active?.panel.isRunning()) {
if (active !== undefined && this.shouldCancelOnUnmount(active.panel)) {
void this.cancelAgent(active.agentId);
}
this.active = undefined;
Expand All @@ -64,9 +64,9 @@ export class BtwPanelController {
closeOrCancel(): boolean {
const active = this.active;
if (active === undefined) return false;
const wasRunning = active.panel.isRunning();
const shouldCancel = this.shouldCancelOnUnmount(active.panel);
this.close(active.panel);
if (wasRunning) {
if (shouldCancel) {
void this.cancelAgent(active.agentId);
}
return true;
Expand Down Expand Up @@ -213,6 +213,10 @@ export class BtwPanelController {
});
}

private shouldCancelOnUnmount(panel: BtwPanelComponent): boolean {
return panel.isRunning() || panel.isEmpty();
}

private withInteractiveAgent<T>(agentId: string, fn: () => Promise<T>): Promise<T> {
const previousAgentId = this.host.harness.interactiveAgentId;
this.host.harness.interactiveAgentId = agentId;
Expand Down
5 changes: 5 additions & 0 deletions apps/kimi-code/test/tui/commands/resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ describe('resolveSlashCommandInput', () => {
args: 'New title',
});
expect(resolve('/init')).toMatchObject({ kind: 'builtin', name: 'init', args: '' });
expect(resolve('/btw')).toMatchObject({
kind: 'builtin',
name: 'btw',
args: '',
});
expect(resolve('/btw what are you doing?')).toMatchObject({
kind: 'builtin',
name: 'btw',
Expand Down
65 changes: 51 additions & 14 deletions apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1100,15 +1100,15 @@ describe('KimiTUI message flow', () => {
coalescedCount: 1,
stale: false,
},
prompt: '提醒用户:这是每分钟提醒',
prompt: 'Remind the user: this is a once-per-minute reminder',
} as Event,
vi.fn(),
);

const entry = driver.state.transcriptEntries.at(-1);
expect(entry).toMatchObject({
kind: 'cron',
content: '提醒用户:这是每分钟提醒',
content: 'Remind the user: this is a once-per-minute reminder',
cronData: {
jobId: 'deadbeef',
cron: '* * * * *',
Expand All @@ -1120,7 +1120,7 @@ describe('KimiTUI message flow', () => {
const transcript = stripSgr(driver.state.transcriptContainer.render(120).join('\n'));
expect(transcript).toContain('Scheduled reminder fired');
expect(transcript).toContain('* * * * *');
expect(transcript).toContain('提醒用户:这是每分钟提醒');
expect(transcript).toContain('Remind the user: this is a once-per-minute reminder');
expect(transcript).not.toContain('<cron-fire');
});

Expand Down Expand Up @@ -1352,32 +1352,68 @@ describe('KimiTUI message flow', () => {
driver.state.appState.streamingPhase = 'composing';
driver.state.livePane.mode = 'thinking';

driver.handleUserInput('/btw 你现在在做啥?');
driver.handleUserInput('/btw What are you working on right now?');

await vi.waitFor(() => {
expect(session.startBtw).toHaveBeenCalledWith();
});
await vi.waitFor(() => {
expect(session.prompt).toHaveBeenCalledWith('你现在在做啥?');
expect(session.prompt).toHaveBeenCalledWith('What are you working on right now?');
});
expect(session.steer).not.toHaveBeenCalled();
expect(driver.state.appState.streamingPhase).toBe('composing');
expect(driver.state.livePane.mode).toBe('thinking');
expect(harness.track).toHaveBeenCalledWith('input_command', { command: 'btw' });
});

it('opens /btw without a question and sends the first panel input to a side agent', async () => {
const session = makeSession();
const { driver } = await makeDriver(session);

driver.handleUserInput('/btw');

await vi.waitFor(() => {
expect(session.startBtw).toHaveBeenCalledWith();
});
expect(session.prompt).not.toHaveBeenCalled();
expect(stripSgr(renderBtwPanel(driver))).toContain('Ready for a side question...');

driver.handleUserInput('What are you working on right now?');

await vi.waitFor(() => {
expect(session.prompt).toHaveBeenCalledWith('What are you working on right now?');
});
expect(session.steer).not.toHaveBeenCalled();
expect(stripSgr(renderBtwPanel(driver))).toContain('Q: What are you working on right now?');
});

it('cancels an unused /btw side agent when closing an empty panel', async () => {
const session = makeSession();
const { driver } = await makeDriver(session);

driver.handleUserInput('/btw');

await vi.waitFor(() => {
expect(session.startBtw).toHaveBeenCalledWith();
});
driver.state.editor.onEscape?.();

expect(session.cancel).toHaveBeenCalledOnce();
expect(driver.state.btwPanelContainer.children).toHaveLength(0);
});

it('renders /btw output in a dedicated panel instead of an Agent tool card', async () => {
const session = makeSession();
const { driver } = await makeDriver(session);
await openBtwPanel(driver, session, '你现在在做啥?');
await openBtwPanel(driver, session, 'What are you working on right now?');

driver.sessionEventHandler.handleEvent(
{
type: 'assistant.delta',
agentId: 'agent-btw',
sessionId: 'ses-1',
turnId: 0,
delta: '正在实现 /btw 的独立面板。',
delta: 'I am implementing the dedicated /btw panel.',
} as Event,
() => {},
);
Expand Down Expand Up @@ -1414,12 +1450,12 @@ describe('KimiTUI message flow', () => {
expect(panel).not.toContain('BTW failed');
expect(panel).not.toContain('Ask:');
expect(panel).not.toContain('Type follow-up');
expect(panel).toContain('Q: 你现在在做啥?');
expect(panel).toContain('正在实现 /btw 的独立面板。');
expect(panel).toContain('Q: What are you working on right now?');
expect(panel).toContain('I am implementing the dedicated /btw panel.');
expect(panel).not.toContain('Agent');
expect(transcript).not.toContain('BTW');
expect(transcript).not.toContain('Esc close');
expect(transcript).not.toContain('正在实现 /btw 的独立面板。');
expect(transcript).not.toContain('I am implementing the dedicated /btw panel.');
});

it('keeps the /btw panel closest to the input after later transcript output', async () => {
Expand Down Expand Up @@ -1921,15 +1957,16 @@ describe('KimiTUI message flow', () => {
expect(renderedPanel).toContain('answer from new side agent');
});

it('does not run /btw without a question or selected model', async () => {
it('does not run /btw without a selected model', async () => {
const { driver, session } = await makeDriver();

driver.state.appState.model = '';
driver.handleUserInput('/btw');
expect(session.startBtw).not.toHaveBeenCalled();
expect(stripSgr(renderTranscript(driver))).toContain('Usage: /btw <question>');
expect(driver.state.btwPanelContainer.children).toHaveLength(0);
expect(stripSgr(renderTranscript(driver))).toContain('LLM not set');

driver.state.appState.model = '';
driver.handleUserInput('/btw 现在在做什么?');
driver.handleUserInput('/btw What are you doing now?');

expect(session.startBtw).not.toHaveBeenCalled();
expect(stripSgr(renderTranscript(driver))).toContain('LLM not set');
Expand Down
2 changes: 1 addition & 1 deletion docs/en/reference/slash-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Prompt mode exits with code `0` when the goal completes, `3` when it blocks, and
| Command | Alias | Description | Always available |
| --- | --- | --- | --- |
| `/help` | `/h`, `/?` | Show keyboard shortcuts and all available commands. | Yes |
| `/btw <question>` | — | Open a side-channel conversation in a forked subagent without steering the current main agent turn. | Yes |
| `/btw [question]` | — | Open a side-channel conversation in a forked subagent without steering the current main agent turn. Without a question, it opens the panel and waits for input. | Yes |
| `/usage` | — | Show token usage, context consumption, and quota information. | Yes |
| `/status` | — | Show the current session runtime status, including version, model, working directory, and permission mode. | Yes |
| `/mcp` | — | List the MCP servers in the current session and their connection status. | Yes |
Expand Down
2 changes: 1 addition & 1 deletion docs/zh/reference/slash-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Prompt 模式在 goal 完成时以退出码 `0` 退出,在 blocked 时以 `3`
| 命令 | 别名 | 说明 | 随时可用 |
| --- | --- | --- | --- |
| `/help` | `/h`、`/?` | 显示快捷键和所有可用命令。 | 是 |
| `/btw <问题>` | — | 在 fork 出的子 Agent 中打开旁路对话,不改变当前主 Agent 轮次。 | 是 |
| `/btw [问题]` | — | 在 fork 出的子 Agent 中打开旁路对话,不改变当前主 Agent 轮次;不带问题时会先打开面板并等待输入。 | 是 |
| `/usage` | — | 显示 token 用量、上下文占用以及配额信息。 | 是 |
| `/status` | — | 显示当前会话运行时状态,包括版本、模型、工作目录和权限模式等。 | 是 |
| `/mcp` | — | 列出当前会话中的 MCP server 及其连接状态。 | 是 |
Expand Down
Loading