diff --git a/.changeset/v2-task-notification-replay.md b/.changeset/v2-task-notification-replay.md new file mode 100644 index 00000000000..244bf72da28 --- /dev/null +++ b/.changeset/v2-task-notification-replay.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix resumed sessions rendering background task completion notifications as raw protocol text instead of a task status card. diff --git a/apps/kimi-code/src/tui/controllers/session-replay.ts b/apps/kimi-code/src/tui/controllers/session-replay.ts index f1a027a0219..e809defcd61 100644 --- a/apps/kimi-code/src/tui/controllers/session-replay.ts +++ b/apps/kimi-code/src/tui/controllers/session-replay.ts @@ -3,7 +3,6 @@ import type { ContextMessage, GoalChange, PermissionMode, - PromptOrigin, ResumedAgentState, Session, ToolCall, @@ -42,6 +41,7 @@ import { pluginCommandFromOrigin, toolCallFromReplayMessage, toolResultOutput, + type BackgroundTaskNotificationOrigin, type ReplayRenderContext, type SkillActivationProjection, type PluginCommandProjection, @@ -667,7 +667,7 @@ export class SessionReplayRenderer { private renderBackgroundTaskNotification( context: ReplayRenderContext, - origin: Extract, + origin: BackgroundTaskNotificationOrigin, ): void { const { sessionEventHandler } = this.host; const task = sessionEventHandler.backgroundTasks.get(origin.taskId); diff --git a/apps/kimi-code/src/tui/utils/message-replay.ts b/apps/kimi-code/src/tui/utils/message-replay.ts index d778b9a4721..cf9cef5702b 100644 --- a/apps/kimi-code/src/tui/utils/message-replay.ts +++ b/apps/kimi-code/src/tui/utils/message-replay.ts @@ -1,6 +1,7 @@ import type { AgentReplayRecord, BackgroundTaskInfo, + BackgroundTaskStatus, ContentPart, ContextMessage, PromptOrigin, @@ -204,10 +205,26 @@ export function contentPartsToText(content: readonly ContentPart[]): string { return content.map(contentPartToText).join(''); } +/** + * agent-core-v2's task domain persists the terminal notification under the + * 'task' spelling (v1 used 'background_task'); both reach replay verbatim. + */ +export interface TaskNotificationOrigin { + readonly kind: 'task'; + readonly taskId: string; + readonly status: BackgroundTaskStatus; + readonly notificationId: string; +} + +export type BackgroundTaskNotificationOrigin = + | Extract + | TaskNotificationOrigin; + export function backgroundOrigin( message: ContextMessage, -): Extract | undefined { - return message.origin?.kind === 'background_task' ? message.origin : undefined; +): BackgroundTaskNotificationOrigin | undefined { + const origin = message.origin as BackgroundTaskNotificationOrigin | undefined; + return origin?.kind === 'background_task' || origin?.kind === 'task' ? origin : undefined; } export function skillActivationFromOrigin( diff --git a/apps/kimi-code/test/tui/message-replay.test.ts b/apps/kimi-code/test/tui/message-replay.test.ts index d944a485f87..c901374a2ab 100644 --- a/apps/kimi-code/test/tui/message-replay.test.ts +++ b/apps/kimi-code/test/tui/message-replay.test.ts @@ -25,6 +25,7 @@ import { } from '#/tui/utils/transcript-window'; import { ToolCallComponent } from '#/tui/components/messages/tool-call'; import { ReadGroupComponent } from '#/tui/components/messages/read-group'; +import type { TaskNotificationOrigin } from '#/tui/utils/message-replay'; vi.mock('#/utils/open-url', () => ({ openUrl: vi.fn() })); @@ -78,7 +79,7 @@ function message( extra: { readonly toolCalls?: readonly ToolCall[]; readonly toolCallId?: string; - readonly origin?: PromptOrigin; + readonly origin?: PromptOrigin | TaskNotificationOrigin; readonly isError?: boolean; } = {}, ): AgentReplayRecord { @@ -90,7 +91,7 @@ function message( content: [...content], toolCalls: [...(extra.toolCalls ?? [])], toolCallId: extra.toolCallId, - origin: extra.origin, + origin: extra.origin as PromptOrigin | undefined, isError: extra.isError, }, }; @@ -906,6 +907,46 @@ describe('KimiTUI resume message replay', () => { expect(status?.backgroundAgentStatus?.headline).not.toContain('agent'); }); + it('renders replayed v2 task notifications (task origin) as bash tasks', async () => { + const driver = await replayIntoDriver( + [ + message( + 'user', + [ + { + type: 'text', + text: '\nTitle: Background process completed\n', + }, + ], + { + origin: { + kind: 'task', + taskId: 'bash-done0000', + status: 'completed', + notificationId: 'task:bash-done0000:completed', + }, + }, + ), + ], + { + background: [backgroundTask('bash-done0000', 'Codex comment poller', 'completed')], + }, + ); + + const status = driver.state.transcriptEntries.find( + (entry) => entry.backgroundAgentStatus !== undefined, + ); + + expect(status?.backgroundAgentStatus?.headline).toBe('bash task completed in background'); + expect(status?.backgroundAgentStatus?.detail).toContain('Codex comment poller'); + // The raw notification XML must not leak into the visible transcript. + expect( + driver.state.transcriptEntries.some( + (entry) => entry.kind === 'user' && entry.content.includes(' { const replay = Array.from({ length: 12 }, (_, index) => [ message('user', [{ type: 'text', text: `prompt ${index}` }]),