mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
Implements the locked P3 design end-to-end: - subagent lifecycle projection (spawned→started→suspended→completed/failed) + inline Agent / AgentGroup cards; swarm progress card (multi-column) derived from swarmIndex; goal dock strip (expandable) from goal.updated; plan/goal/ swarm activation badges in the composer status line. - terminal as a view (xterm + WS terminal_* frames with since_seq replay) and a tab/view-dimension split (usePaneLayout tree + ViewGroup + SplitLayout, VSCode editor-group style), persisted to localStorage. Adds swarm-groups / subagent-goal / agent-group-turns unit tests and stub-daemon seeds. 98 tests pass; vue-tsc + oxlint clean; production build OK. Accepted by review (see reports/web-p3-acceptance.md); no blocking issues.
83 lines
2.3 KiB
TypeScript
83 lines
2.3 KiB
TypeScript
import type { AppSubagentPhase, AppTask } from '../api/types';
|
|
|
|
export interface SwarmMember {
|
|
id: string;
|
|
name: string;
|
|
subagentType?: string;
|
|
phase: AppSubagentPhase;
|
|
summary?: string;
|
|
suspendedReason?: string;
|
|
swarmIndex: number;
|
|
}
|
|
|
|
export interface SwarmGroup {
|
|
id: string;
|
|
members: SwarmMember[];
|
|
counts: Record<AppSubagentPhase, number>;
|
|
}
|
|
|
|
const PHASES: readonly AppSubagentPhase[] = ['queued', 'working', 'suspended', 'completed', 'failed'];
|
|
|
|
function phaseForTask(task: AppTask): AppSubagentPhase {
|
|
if (task.subagentPhase) return task.subagentPhase;
|
|
if (task.status === 'completed') return 'completed';
|
|
if (task.status === 'failed') return 'failed';
|
|
return 'working';
|
|
}
|
|
|
|
function emptyCounts(): Record<AppSubagentPhase, number> {
|
|
return {
|
|
queued: 0,
|
|
working: 0,
|
|
suspended: 0,
|
|
completed: 0,
|
|
failed: 0,
|
|
};
|
|
}
|
|
|
|
export function buildSwarmGroups(tasks: AppTask[]): SwarmGroup[] {
|
|
const buckets = new Map<string, SwarmMember[]>();
|
|
|
|
for (const task of tasks) {
|
|
if (task.kind !== 'subagent' || task.swarmIndex === undefined) continue;
|
|
const key = task.parentToolCallId ?? 'swarm';
|
|
const list = buckets.get(key) ?? [];
|
|
list.push({
|
|
id: task.id,
|
|
name: task.description,
|
|
subagentType: task.subagentType,
|
|
phase: phaseForTask(task),
|
|
summary: task.outputPreview,
|
|
suspendedReason: task.suspendedReason,
|
|
swarmIndex: task.swarmIndex,
|
|
});
|
|
buckets.set(key, list);
|
|
}
|
|
|
|
return [...buckets.entries()]
|
|
.map(([id, members]) => {
|
|
const sorted = members.toSorted((a, b) => a.swarmIndex - b.swarmIndex || a.id.localeCompare(b.id));
|
|
const counts = emptyCounts();
|
|
for (const member of sorted) counts[member.phase]++;
|
|
return { id, members: sorted, counts };
|
|
})
|
|
.filter((group) => group.members.length > 1)
|
|
.toSorted((a, b) => {
|
|
const ai = a.members.at(0)?.swarmIndex ?? 0;
|
|
const bi = b.members.at(0)?.swarmIndex ?? 0;
|
|
if (ai !== bi) return ai - bi;
|
|
return a.id.localeCompare(b.id);
|
|
});
|
|
}
|
|
|
|
export function countSwarmMembers(groups: SwarmGroup[]): { done: number; total: number } {
|
|
let done = 0;
|
|
let total = 0;
|
|
for (const group of groups) {
|
|
total += group.members.length;
|
|
for (const phase of PHASES) {
|
|
if (phase === 'completed' || phase === 'failed') done += group.counts[phase];
|
|
}
|
|
}
|
|
return { done, total };
|
|
}
|