diff --git a/.changeset/tidy-crons-run.md b/.changeset/tidy-crons-run.md new file mode 100644 index 0000000000..189bc17a92 --- /dev/null +++ b/.changeset/tidy-crons-run.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Show a footer badge while a scheduled prompt is running. diff --git a/apps/kimi-code/src/tui/components/chrome/footer.ts b/apps/kimi-code/src/tui/components/chrome/footer.ts index 4bb8a75f9b..7ae1cd4d17 100644 --- a/apps/kimi-code/src/tui/components/chrome/footer.ts +++ b/apps/kimi-code/src/tui/components/chrome/footer.ts @@ -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; @@ -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[] { @@ -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); diff --git a/apps/kimi-code/src/tui/controllers/session-event-handler.ts b/apps/kimi-code/src/tui/controllers/session-event-handler.ts index b82f919610..fde2f86122 100644 --- a/apps/kimi-code/src/tui/controllers/session-event-handler.ts +++ b/apps/kimi-code/src/tui/controllers/session-event-handler.ts @@ -155,6 +155,7 @@ export class SessionEventHandler { mcpServers: Map = new Map(); private goalCompletionAwaitingClear = false; private goalCompletionTurnEnded = false; + private currentTurnIsCron = false; private currentTurnHasAssistantText = false; private pluginCommandTurns: Map = new Map(); private pluginMcpToolsUsedInTurn: Set = new Set(); @@ -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(); @@ -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); @@ -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(), @@ -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(); diff --git a/apps/kimi-code/test/tui/components/panels/footer-bg-agents.test.ts b/apps/kimi-code/test/tui/components/panels/footer-bg-agents.test.ts index 101cd19116..bd85150ae9 100644 --- a/apps/kimi-code/test/tui/components/panels/footer-bg-agents.test.ts +++ b/apps/kimi-code/test/tui/components/panels/footer-bg-agents.test.ts @@ -34,12 +34,13 @@ function baseState(overrides: Partial = {}): 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', () => { @@ -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 }); diff --git a/apps/kimi-code/test/tui/controllers/session-event-handler-plugin-updates.test.ts b/apps/kimi-code/test/tui/controllers/session-event-handler-plugin-updates.test.ts index 3220d1b916..1bca18ddb5 100644 --- a/apps/kimi-code/test/tui/controllers/session-event-handler-plugin-updates.test.ts +++ b/apps/kimi-code/test/tui/controllers/session-event-handler-plugin-updates.test.ts @@ -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: {}, 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 b46eab17bf..6435e54bd6 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 @@ -2228,6 +2228,101 @@ command = "vim" expect(transcript).not.toContain(' { + 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 { diff --git a/docs/en/reference/tools.md b/docs/en/reference/tools.md index 8b412b5366..d5f9299532 100644 --- a/docs/en/reference/tools.md +++ b/docs/en/reference/tools.md @@ -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 | diff --git a/docs/zh/reference/tools.md b/docs/zh/reference/tools.md index 009ff3d052..16cdc6bde4 100644 --- a/docs/zh/reference/tools.md +++ b/docs/zh/reference/tools.md @@ -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 |