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/reduce-group-refreshes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Reduce redundant transcript refresh work for grouped agent and file-read activity.
10 changes: 1 addition & 9 deletions apps/kimi-code/src/tui/components/messages/agent-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export class AgentGroupComponent extends Container {
private readonly bodyContainer: Container;
private throttleTimer: ReturnType<typeof setTimeout> | null = null;
private lastFlushPhases = new Map<string, ToolCallSubagentSnapshot['phase']>();
private _invalidating = false;

constructor(private readonly ui: TUI | undefined) {
super();
Expand Down Expand Up @@ -85,7 +84,6 @@ export class AgentGroupComponent extends Container {
tc.setSnapshotListener(() => {
this.scheduleRender();
});
Comment on lines 84 to 86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Flush newly attached agents with no phase

When an Agent tool call is grouped before its subagent.spawned event, getSubagentSnapshot().phase is still undefined; the immediate listener callback now goes through scheduleRender(), where detectPhaseTransition() compares undefined to the missing lastFlushPhases entry and returns false. In the existing group path (tryAttachAgentToolCall), no other render is requested for cur.group.attach(...), so the newly attached agent can remain absent/stale until the 200 ms throttle fires; during solo-to-group upgrade it can even render the empty 0 agents finished snapshot produced by transcriptContainer.invalidate(). Keep an immediate first flush for new entries, or treat a missing phase entry as a transition even when the phase is undefined.

Useful? React with 👍 / 👎.

this.flushRender();
}

/**
Expand Down Expand Up @@ -143,7 +141,7 @@ export class AgentGroupComponent extends Container {
if (snap !== undefined) this.lastFlushPhases.set(entry.toolCallId, snap.phase);
});

this.invalidate();
super.invalidate();
this.ui?.requestRender();
}

Expand Down Expand Up @@ -225,13 +223,7 @@ export class AgentGroupComponent extends Container {

/** Releases throttle timers so destroyed components cannot refresh later. */
override invalidate(): void {
if (this._invalidating) {
super.invalidate();
return;
}
this._invalidating = true;
this.flushRender();
this._invalidating = false;
}

dispose(): void {
Expand Down
10 changes: 1 addition & 9 deletions apps/kimi-code/src/tui/components/messages/read-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export class ReadGroupComponent extends Container {
private readonly bodyContainer: Container;
private throttleTimer: ReturnType<typeof setTimeout> | null = null;
private lastFlushPhases = new Map<string, ToolCallReadSnapshot['phase']>();
private _invalidating = false;

constructor(private readonly ui: TUI | undefined) {
super();
Expand All @@ -67,7 +66,6 @@ export class ReadGroupComponent extends Container {
tc.setSnapshotListener(() => {
this.scheduleRender();
});
this.flushRender();
}

/**
Expand Down Expand Up @@ -126,7 +124,7 @@ export class ReadGroupComponent extends Container {
if (snap !== undefined) this.lastFlushPhases.set(entry.toolCallId, snap.phase);
});

this.invalidate();
super.invalidate();
this.ui?.requestRender();
}

Expand Down Expand Up @@ -171,13 +169,7 @@ export class ReadGroupComponent extends Container {
}

override invalidate(): void {
if (this._invalidating) {
super.invalidate();
return;
}
this._invalidating = true;
this.flushRender();
this._invalidating = false;
}

/** Releases throttle timers so destroyed components cannot refresh later. */
Expand Down
17 changes: 17 additions & 0 deletions apps/kimi-code/test/tui/components/messages/agent-group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ describe('AgentGroupComponent', () => {
vi.useRealTimers();
});

it('requests one render when attaching an agent', () => {
vi.useFakeTimers();
vi.setSystemTime(0);
const ui = stubTui();
const group = new AgentGroupComponent(ui);
const agent = createAgent('call_agent_1', 'inspect project', 'reviewer', ui);

vi.mocked(ui.requestRender).mockClear();

group.attach('call_agent_1', agent);

expect(ui.requestRender).toHaveBeenCalledTimes(1);

group.dispose();
agent.dispose();
});

it('shows explicit active breakdown, row state, and waiting fallback', () => {
vi.useFakeTimers();
vi.setSystemTime(0);
Expand Down
41 changes: 41 additions & 0 deletions apps/kimi-code/test/tui/components/messages/read-group.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { TUI } from '@moonshot-ai/pi-tui';
import { describe, expect, it, vi } from 'vitest';

import { ReadGroupComponent } from '#/tui/components/messages/read-group';
import { ToolCallComponent } from '#/tui/components/messages/tool-call';

function stubTui(): TUI {
return {
terminal: { rows: 40 },
requestRender: vi.fn(),
} as unknown as TUI;
}

function createRead(id: string, filePath: string, ui: TUI): ToolCallComponent {
return new ToolCallComponent(
{
id,
name: 'Read',
args: { file_path: filePath },
},
undefined,
ui,
);
}

describe('ReadGroupComponent', () => {
it('requests one render when attaching a read', () => {
const ui = stubTui();
const group = new ReadGroupComponent(ui);
const read = createRead('call_read_1', 'src/a.ts', ui);

vi.mocked(ui.requestRender).mockClear();

group.attach('call_read_1', read);

expect(ui.requestRender).toHaveBeenCalledTimes(1);

group.dispose();
read.dispose();
});
});