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/v2-task-notification-replay.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions apps/kimi-code/src/tui/controllers/session-replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type {
ContextMessage,
GoalChange,
PermissionMode,
PromptOrigin,
ResumedAgentState,
Session,
ToolCall,
Expand Down Expand Up @@ -42,6 +41,7 @@ import {
pluginCommandFromOrigin,
toolCallFromReplayMessage,
toolResultOutput,
type BackgroundTaskNotificationOrigin,
type ReplayRenderContext,
type SkillActivationProjection,
type PluginCommandProjection,
Expand Down Expand Up @@ -667,7 +667,7 @@ export class SessionReplayRenderer {

private renderBackgroundTaskNotification(
context: ReplayRenderContext,
origin: Extract<PromptOrigin, { kind: 'background_task' }>,
origin: BackgroundTaskNotificationOrigin,
): void {
const { sessionEventHandler } = this.host;
const task = sessionEventHandler.backgroundTasks.get(origin.taskId);
Expand Down
21 changes: 19 additions & 2 deletions apps/kimi-code/src/tui/utils/message-replay.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
AgentReplayRecord,
BackgroundTaskInfo,
BackgroundTaskStatus,
ContentPart,
ContextMessage,
PromptOrigin,
Expand Down Expand Up @@ -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<PromptOrigin, { kind: 'background_task' }>
| TaskNotificationOrigin;

export function backgroundOrigin(
message: ContextMessage,
): Extract<PromptOrigin, { kind: 'background_task' }> | 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(
Expand Down
45 changes: 43 additions & 2 deletions apps/kimi-code/test/tui/message-replay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() }));

Expand Down Expand Up @@ -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 {
Expand All @@ -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,
},
};
Expand Down Expand Up @@ -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: '<notification id="task:bash-done0000:completed" category="task" type="task.completed" source_kind="background_task" source_id="bash-done0000">\nTitle: Background process completed\n</notification>',
},
],
{
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('<notification'),
),
).toBe(false);
});

it('renders only the most recent ten visible user turns', async () => {
const replay = Array.from({ length: 12 }, (_, index) => [
message('user', [{ type: 'text', text: `prompt ${index}` }]),
Expand Down
Loading