Skip to content
Open
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/tidy-crons-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Show a footer badge while a scheduled prompt is running.
8 changes: 8 additions & 0 deletions apps/kimi-code/src/tui/components/chrome/footer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export class FooterComponent implements Component {
*/
private backgroundBashTaskCount = 0;
private backgroundAgentCount = 0;
private cronRunning = false;

constructor(state: AppState, onRefresh: () => void = () => {}) {
this.state = state;
Expand Down Expand Up @@ -268,6 +269,10 @@ export class FooterComponent implements Component {
this.backgroundAgentCount = Math.max(0, counts.agentTasks);
}

setCronRunning(running: boolean): void {
this.cronRunning = running;
}

invalidate(): void {}

render(width: number): string[] {
Expand Down Expand Up @@ -424,6 +429,9 @@ export class FooterComponent implements Component {
chalk.hex(colors.primary)(`[${String(this.backgroundAgentCount)} ${noun} running]`),
);
}
if (this.cronRunning) {
taskBadges.push(chalk.hex(colors.primary)('[cron running]'));
}
slots['tasks'] = taskBadges;

const cwd = shortenCwd(state.workDir);
Expand Down
11 changes: 11 additions & 0 deletions apps/kimi-code/src/tui/controllers/session-event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export class SessionEventHandler {
mcpServers: Map<string, McpServerStatusSnapshot> = new Map();
private goalCompletionAwaitingClear = false;
private goalCompletionTurnEnded = false;
private currentTurnIsCron = false;
private currentTurnHasAssistantText = false;
private pluginCommandTurns: Map<string, string> = new Map();
private pluginMcpToolsUsedInTurn: Set<string> = new Set();
Expand All @@ -173,6 +174,8 @@ export class SessionEventHandler {
this.mcpServers.clear();
this.goalCompletionAwaitingClear = false;
this.goalCompletionTurnEnded = false;
this.currentTurnIsCron = false;
this.host.state.footer.setCronRunning(false);
this.currentTurnHasAssistantText = false;
this.pluginCommandTurns.clear();
this.pluginMcpToolsUsedInTurn.clear();
Expand Down Expand Up @@ -312,6 +315,8 @@ export class SessionEventHandler {
// ---------------------------------------------------------------------------

private handleTurnBegin(event: TurnStartedEvent): void {
this.currentTurnIsCron = event.origin?.kind === 'cron_job';
this.host.state.footer.setCronRunning(this.currentTurnIsCron);
this.currentTurnHasAssistantText = false;
if (event.origin?.kind === 'plugin_command') {
this.pluginCommandTurns.set(String(event.turnId), event.origin.pluginId);
Expand All @@ -331,6 +336,8 @@ export class SessionEventHandler {
}

private handleCronFired(event: CronFiredEvent): void {
this.currentTurnIsCron = true;
this.host.state.footer.setCronRunning(true);
this.host.streamingUI.flushNow();
this.host.appendTranscriptEntry({
id: nextTranscriptId(),
Expand All @@ -349,6 +356,10 @@ export class SessionEventHandler {
}

private handleTurnEnd(event: TurnEndedEvent, sendQueued: (item: QueuedMessage) => void): void {
if (this.currentTurnIsCron) {
this.currentTurnIsCron = false;
this.host.state.footer.setCronRunning(false);
}
this.host.streamingUI.flushNow();
if (event.reason === 'cancelled') {
this.markActiveAgentSwarmsCancelled();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ function baseState(overrides: Partial<AppState> = {}): AppState {
}

describe('FooterComponent — background task / agent badges', () => {
it('omits both badges when counts are 0', () => {
it('omits all background badges when counts are 0', () => {
const footer = new FooterComponent(baseState());
const [line1] = footer.render(120);
expect(line1).toBeDefined();
expect(strip(line1!)).not.toMatch(/tasks? running/);
expect(strip(line1!)).not.toMatch(/agents? running/);
expect(strip(line1!)).not.toMatch(/cron running/);
});

it('renders the task badge alone when only bash tasks are running', () => {
Expand Down Expand Up @@ -68,6 +69,30 @@ describe('FooterComponent — background task / agent badges', () => {
expect(out.indexOf('2 tasks')).toBeLessThan(out.indexOf('3 agents'));
});

it('renders a cron badge while a scheduled prompt is running', () => {
const footer = new FooterComponent(baseState());
footer.setCronRunning(true);

expect(strip(footer.render(120)[0]!)).toMatch(/\[cron running\]/);
});

it('renders cron alongside other background activity and clears it independently', () => {
const footer = new FooterComponent(baseState());
footer.setBackgroundCounts({ bashTasks: 1, agentTasks: 1 });
footer.setCronRunning(true);

const active = strip(footer.render(120)[0]!);
expect(active).toMatch(/\[1 task running\]/);
expect(active).toMatch(/\[1 agent running\]/);
expect(active).toMatch(/\[cron running\]/);

footer.setCronRunning(false);
const completed = strip(footer.render(120)[0]!);
expect(completed).toMatch(/\[1 task running\]/);
expect(completed).toMatch(/\[1 agent running\]/);
expect(completed).not.toMatch(/cron running/);
});

it('pluralizes correctly across both badges', () => {
const footer = new FooterComponent(baseState());
footer.setBackgroundCounts({ bashTasks: 1, agentTasks: 1 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function makeHost() {
toolOutputExpanded: false,
todoPanel: { getTodos: vi.fn(() => []) },
transcriptContainer: { addChild: vi.fn() },
footer: { setCronRunning: vi.fn() },
ui: { requestRender: vi.fn() },
},
session: {},
Expand Down
95 changes: 95 additions & 0 deletions apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2228,6 +2228,101 @@ command = "vim"
expect(transcript).not.toContain('<cron-fire');
});

it('shows cron status only while a cron-triggered turn is running', async () => {
const { driver } = await makeDriver();
const origin = {
kind: 'cron_job' as const,
jobId: 'deadbeef',
cron: '* * * * *',
recurring: true,
coalescedCount: 1,
stale: false,
};

driver.sessionEventHandler.handleEvent(
{
type: 'cron.fired',
agentId: 'main',
sessionId: 'ses-1',
origin,
prompt: 'Run the scheduled check',
} as Event,
vi.fn(),
);
driver.sessionEventHandler.handleEvent(
{
type: 'turn.started',
agentId: 'main',
sessionId: 'ses-1',
turnId: 1,
origin,
} as Event,
vi.fn(),
);

expect(stripSgr(driver.state.footer.render(160)[0]!)).toContain('[cron running]');

driver.sessionEventHandler.handleEvent(
{
type: 'turn.ended',
agentId: 'main',
sessionId: 'ses-1',
turnId: 1,
reason: 'completed',
} as Event,
vi.fn(),
);

expect(stripSgr(driver.state.footer.render(160)[0]!)).not.toContain('[cron running]');
});

it('shows cron status when a scheduled prompt is steered into an active user turn', async () => {
const { driver } = await makeDriver();

driver.sessionEventHandler.handleEvent(
{
type: 'turn.started',
agentId: 'main',
sessionId: 'ses-1',
turnId: 1,
origin: { kind: 'user' },
} as Event,
vi.fn(),
);
driver.sessionEventHandler.handleEvent(
{
type: 'cron.fired',
agentId: 'main',
sessionId: 'ses-1',
origin: {
kind: 'cron_job',
jobId: 'cafebabe',
cron: '*/5 * * * *',
recurring: true,
coalescedCount: 1,
stale: false,
},
prompt: 'Run the scheduled check',
} as Event,
vi.fn(),
);

expect(stripSgr(driver.state.footer.render(160)[0]!)).toContain('[cron running]');

driver.sessionEventHandler.handleEvent(
{
type: 'turn.ended',
agentId: 'main',
sessionId: 'ses-1',
turnId: 1,
reason: 'completed',
} as Event,
vi.fn(),
);

expect(stripSgr(driver.state.footer.render(160)[0]!)).not.toContain('[cron running]');
});

it('coalesces assistant delta component updates', async () => {
vi.useFakeTimers();
try {
Expand Down
2 changes: 2 additions & 0 deletions docs/en/reference/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ Background task tools manage tasks started via `Bash`, `Agent`, or `AskUserQuest

Scheduled task tools allow the Agent to re-inject a prompt into the current session at a future time — either as a one-time reminder or as a recurring cron-triggered task (periodic checks, daily reports, deployment monitoring, etc.). Schedules are bound to the session and remain active when you resume it with `kimi --session`, but are not carried into a brand-new session. A single session can hold at most 50 active scheduled tasks. Set `KIMI_DISABLE_CRON=1` to disable them entirely; see [Environment Variables](../configuration/env-vars.md#runtime-switches).

When a scheduled prompt is actively running, the terminal footer shows `[cron running]`. The badge disappears when that turn finishes; schedules that are merely waiting for their next fire do not show it.

| Tool | Default Approval | Description |
| --- | --- | --- |
| `CronCreate` | Requires approval | Schedule a prompt to fire at a future time |
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/reference/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ Plan 模式是一种受约束的工作状态:进入后 `Write` 与 `Edit` 只

定时任务工具允许 Agent 把一段 prompt 在未来某个时间重新注入到当前会话——既可以是一次性提醒,也可以是按 cron 周期触发的任务(定期巡检、每日报表、部署监控等)。计划绑定到会话,用 `kimi --session` 恢复会话后仍然有效,但不会带入全新的会话。单个会话最多保留 50 个生效中的定时任务。设置 `KIMI_DISABLE_CRON=1` 可整体禁用,详见[环境变量](../configuration/env-vars.md#运行时开关)。

定时 prompt 正在运行时,终端 footer 会显示 `[cron running]`。该轮次结束后徽标会消失;仅等待下一次触发的定时任务不会显示徽标。

| 工具 | 默认审批 | 说明 |
| --- | --- | --- |
| `CronCreate` | 需审批 | 安排一个在未来时刻触发的 prompt |
Expand Down