-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(kimi-code): specialize the WaitFor tool's transcript display #3066
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
0080046
feat(kimi-code): specialize the WaitFor tool's transcript display
chengluyu dd6c99a
feat(agent-core-v2): emit status progress while WaitFor is pending
chengluyu 536cc27
fix(kimi-code): route WaitFor dimming through the TUI theme
chengluyu 1e20145
feat(kimi-code): support replaceable status updates in tool progress
chengluyu 369f399
fix(kimi-code): forward status progress to subagent activity surfaces
chengluyu 4f6e7bd
fix(agent-core-v2): drop the redundant undefined from ToolUpdate.replace
chengluyu 1f48298
fix(kimi-code): honor replace semantics in the subagent live status path
chengluyu f38e4cc
test(agent-core-v2): drive the WaitFor progress test through a manual…
chengluyu 255aeaf
fix(kap-server): mirror ToolUpdate.replace in the ws event schema
chengluyu 7e45ab9
refactor(agent-core-v2): expose the WaitFor progress scheduler as a p…
chengluyu c749138
fix(kimi-code): pass child wait statuses without the trailing newline
chengluyu 4b42116
feat(agent-core-v2): tick the WaitFor progress status every second
chengluyu 34d1c38
feat(agent-core-v2): format WaitFor progress durations as 1m 15s
chengluyu 0f6f2a2
feat(agent-core-v2): omit zero seconds and minutes in WaitFor durations
chengluyu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Show a live status line with elapsed time and remaining task count while the WaitFor tool is waiting. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Improve the WaitFor tool's transcript display: the header shows the waited task and its outcome, and the body summarizes the finished task, other tasks that completed during the wait, and tasks still running, instead of dumping raw fields. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
179 changes: 179 additions & 0 deletions
179
apps/kimi-code/src/tui/components/messages/tool-renderers/wait-for.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| /** | ||
| * WaitFor renderer — the wait result is a timeline (header fields, then | ||
| * `[finished]` / `[completed_during_wait]` / `[still_running]` sections), | ||
| * so the collapsed body shows what the wait came back with instead of the | ||
| * raw key-value dump: the finished task with its outcome, plus counts of | ||
| * tasks that finished alongside or are still running. A timeout is not an | ||
| * error (the tool says so itself), so it renders in the warning tone. | ||
| */ | ||
|
|
||
| import { Text, type Component } from '@moonshot-ai/pi-tui'; | ||
|
|
||
| import { STATUS_BULLET } from '#/tui/constant/symbols'; | ||
| import { currentTheme } from '#/tui/theme'; | ||
| import type { ToolCallBlockData, ToolResultBlockData } from '#/tui/types'; | ||
|
|
||
| import { formatGoalElapsed } from '../goal-format'; | ||
| import { renderTruncated } from './truncated'; | ||
| import type { ResultRenderer } from './types'; | ||
|
|
||
| const DESCRIPTION_MAX = 72; | ||
| const RUNNING_SAMPLES = 3; | ||
|
|
||
| type WaitForStatus = 'completed' | 'timed_out' | 'no_tasks'; | ||
|
|
||
| interface WaitForResultView { | ||
| readonly status: WaitForStatus; | ||
| readonly waitedMs: number; | ||
| readonly finishedTaskId?: string; | ||
| readonly finishedStatus?: string; | ||
| readonly finishedDescription?: string; | ||
| readonly extraCount: number; | ||
| readonly runningCount: number; | ||
| readonly runningSamples: readonly string[]; | ||
| } | ||
|
|
||
| export const waitForSummary: ResultRenderer = (toolCall, result, ctx) => { | ||
| if (result.is_error) return renderTruncated(toolCall, result, ctx); | ||
| const view = parseWaitForOutput(result.output); | ||
| if (view === undefined) return renderTruncated(toolCall, result, ctx); | ||
|
|
||
| const out: Component[] = []; | ||
| for (const line of glanceLines(view)) { | ||
| out.push(new Text(` ${currentTheme.dim(line)}`, 0, 0)); | ||
| } | ||
| if (ctx.expanded && result.output.length > 0) { | ||
| out.push(new Text(currentTheme.dim(result.output), 4, 0)); | ||
| } | ||
| return out; | ||
| }; | ||
|
|
||
| export function buildWaitForHeader(options: { | ||
| readonly toolCall: ToolCallBlockData; | ||
| readonly result: ToolResultBlockData | undefined; | ||
| readonly bullet: string; | ||
| readonly chip: string; | ||
| }): string | undefined { | ||
| const { toolCall, result, bullet, chip } = options; | ||
| if (toolCall.name !== 'WaitFor') return undefined; | ||
|
|
||
| const taskId = typeof toolCall.args['task_id'] === 'string' ? toolCall.args['task_id'] : undefined; | ||
| const argText = | ||
| taskId === undefined ? '' : currentTheme.dimFg('textDim', ` (${taskId})`); | ||
|
|
||
| if (result === undefined) { | ||
| const label = | ||
| taskId === undefined ? 'Waiting for any background task' : 'Waiting for background task'; | ||
| return `${bullet}${currentTheme.boldFg('primary', label)}${argText}`; | ||
| } | ||
| if (result.is_error === true) { | ||
| return `${bullet}${currentTheme.boldFg('error', 'Could not wait for background task')}${argText}`; | ||
| } | ||
|
|
||
| const status = parseWaitForOutput(result.output)?.status; | ||
| if (status === 'timed_out') { | ||
| return `${currentTheme.fg('warning', STATUS_BULLET)}${currentTheme.boldFg('warning', 'Wait timed out')}${argText}${chip}`; | ||
| } | ||
| if (status === 'no_tasks') { | ||
| return `${bullet}${currentTheme.boldFg('primary', 'No background tasks running')}${chip}`; | ||
| } | ||
| const label = taskId === undefined ? 'Waited for a background task' : 'Waited for background task'; | ||
| return `${bullet}${currentTheme.boldFg('primary', label)}${argText}${chip}`; | ||
| } | ||
|
|
||
| export const waitForChip = (_toolCall: ToolCallBlockData, result: ToolResultBlockData): string => { | ||
| if (result.is_error === true) return ''; | ||
| const view = parseWaitForOutput(result.output); | ||
| if (view === undefined || view.status === 'no_tasks') return ''; | ||
| return formatGoalElapsed(view.waitedMs); | ||
| }; | ||
|
|
||
| function glanceLines(view: WaitForResultView): string[] { | ||
| switch (view.status) { | ||
| case 'no_tasks': | ||
| return []; | ||
| case 'timed_out': { | ||
| if (view.runningCount === 0) return []; | ||
| const summary = `${pluralizeTasks(view.runningCount)} still running`; | ||
| if (view.runningSamples.length === 0) return [summary]; | ||
| const remaining = view.runningCount - view.runningSamples.length; | ||
| const tail = remaining > 0 ? `, +${String(remaining)} more` : ''; | ||
| return [`${summary}: ${view.runningSamples.join(', ')}${tail}`]; | ||
| } | ||
| case 'completed': { | ||
| const taskId = view.finishedTaskId ?? 'task'; | ||
| const status = view.finishedStatus ?? 'completed'; | ||
| const marker = status === 'completed' ? '✓' : '✗'; | ||
| const description = | ||
| view.finishedDescription === undefined | ||
| ? '' | ||
| : ` · ${truncateOneLine(view.finishedDescription, DESCRIPTION_MAX)}`; | ||
| const lines = [`${marker} ${taskId} ${status}${description}`]; | ||
| const parts: string[] = []; | ||
| if (view.extraCount > 0) parts.push(`+${String(view.extraCount)} more finished during wait`); | ||
| if (view.runningCount > 0) parts.push(`${pluralizeTasks(view.runningCount)} still running`); | ||
| if (parts.length > 0) lines.push(parts.join(' · ')); | ||
| return lines; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function pluralizeTasks(count: number): string { | ||
| return `${String(count)} background task${count === 1 ? '' : 's'}`; | ||
| } | ||
|
|
||
| function parseWaitForOutput(output: string): WaitForResultView | undefined { | ||
| const status = field(output, 'wait_status'); | ||
| if (status !== 'completed' && status !== 'timed_out' && status !== 'no_tasks') return undefined; | ||
| const waitedMs = Number(field(output, 'waited_ms') ?? 0); | ||
| const finished = section(output, 'finished'); | ||
| const duringWait = section(output, 'completed_during_wait'); | ||
| const stillRunning = section(output, 'still_running'); | ||
| const runningCount = stillRunning === undefined ? 0 : countField(stillRunning, 'active_background_tasks'); | ||
| return { | ||
| status, | ||
| waitedMs: Number.isFinite(waitedMs) ? waitedMs : 0, | ||
| finishedTaskId: field(output, 'task_id'), | ||
| finishedStatus: finished === undefined ? undefined : field(finished, 'status'), | ||
| finishedDescription: finished === undefined ? undefined : field(finished, 'description'), | ||
| extraCount: duringWait === undefined ? 0 : countOccurrences(duringWait, /^task_id: /gm), | ||
| runningCount, | ||
| runningSamples: | ||
| stillRunning === undefined ? [] : sampleDescriptions(stillRunning, runningCount), | ||
| }; | ||
| } | ||
|
|
||
| function field(text: string, name: string): string | undefined { | ||
| const match = new RegExp(`^${name}: (.+)$`, 'm').exec(text); | ||
| return match?.[1]; | ||
| } | ||
|
|
||
| function countField(text: string, name: string): number { | ||
| const value = Number(field(text, name) ?? 0); | ||
| return Number.isFinite(value) ? value : 0; | ||
| } | ||
|
|
||
| function section(output: string, name: string): string | undefined { | ||
| const match = new RegExp(`^\\[${name}\\]$`, 'm').exec(output); | ||
| if (match === null) return undefined; | ||
| const rest = output.slice(match.index + match[0].length); | ||
| const next = /^\[/m.exec(rest); | ||
| return (next === null ? rest : rest.slice(0, next.index)).trim(); | ||
| } | ||
|
|
||
| function countOccurrences(text: string, pattern: RegExp): number { | ||
| return text.match(pattern)?.length ?? 0; | ||
| } | ||
|
|
||
| function sampleDescriptions(stillRunning: string, runningCount: number): readonly string[] { | ||
| const descriptions = [...stillRunning.matchAll(/^description: (.+)$/gm)].map((match) => | ||
| truncateOneLine(match[1] ?? '', 40), | ||
| ); | ||
| return descriptions.slice(0, Math.min(RUNNING_SAMPLES, runningCount)); | ||
| } | ||
|
|
||
| function truncateOneLine(text: string, max: number): string { | ||
| const firstLine = text.replaceAll(/\s+/g, ' ').trim(); | ||
| if (firstLine.length <= max) return firstLine; | ||
| return `${firstLine.slice(0, Math.max(0, max - 1))}…`; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.