Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4df6a3d
fix: specialize goal tool display
chengluyu Jun 8, 2026
6a276ac
fix: summarize completed goals
chengluyu Jun 8, 2026
c01a5af
fix: pause goals on runtime errors
chengluyu Jun 8, 2026
1d2fb17
fix: explain blocked goals in messages
chengluyu Jun 8, 2026
c5e7124
fix: suppress duplicate blocked goal marker
chengluyu Jun 8, 2026
cdd7c9e
fix: show goal cancel as notice
chengluyu Jun 8, 2026
e596992
fix: align goal report header colors
chengluyu Jun 8, 2026
e89573c
fix: emphasize goal pause resume markers
chengluyu Jun 8, 2026
ab52441
fix: space goal lifecycle markers
chengluyu Jun 8, 2026
622e378
fix: avoid duplicate goal resume status
chengluyu Jun 8, 2026
6b39226
fix: strengthen goal outcome prompts
chengluyu Jun 8, 2026
f28b8ee
fix: avoid duplicate goal budget header
chengluyu Jun 8, 2026
0dba381
fix: color goal budget marker consistently
chengluyu Jun 8, 2026
9e47c6f
fix: hide goal outcome prompts on replay
chengluyu Jun 8, 2026
4f54a78
fix: respect goal summary step limits
chengluyu Jun 8, 2026
943f333
fix: replay deterministic goal completion stats
chengluyu Jun 8, 2026
efd7fa9
fix: avoid repeated paused marker wording
chengluyu Jun 8, 2026
c8dc3fc
fix: show fallback for unexplained blocked goals
chengluyu Jun 8, 2026
31dbce3
fix: attribute model goal pauses
chengluyu Jun 8, 2026
c415f94
refactor: share goal time formatting
chengluyu Jun 8, 2026
ed0fa18
fix: avoid duplicate blocked fallback
chengluyu Jun 8, 2026
e85ccfa
Merge remote-tracking branch 'origin/main' into fix/goal-nits-2
kermanx Jun 9, 2026
a3fbc1a
refactor: move outcome.ts to usage
kermanx Jun 9, 2026
9b4411a
update
kermanx Jun 9, 2026
188143d
fix
kermanx Jun 9, 2026
d63de2a
fix
kermanx Jun 9, 2026
c539c39
Merge remote-tracking branch 'origin/main' into fix/goal-nits-2
kermanx Jun 9, 2026
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
6 changes: 6 additions & 0 deletions .changeset/goal-mode-outcomes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kimi-code": patch
---

Improve goal mode outcome handling with follow-up messages, safer error pauses, and clearer TUI transcript display.
3 changes: 1 addition & 2 deletions apps/kimi-code/src/tui/commands/goal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,6 @@ async function resumeGoal(host: SlashCommandHost): Promise<void> {
return;
}
host.track('goal_resume');
host.showStatus('Goal resumed.');
host.sendNormalUserInput(RESUME_GOAL_INPUT);
}

Expand All @@ -478,7 +477,7 @@ async function cancelGoal(host: SlashCommandHost): Promise<void> {
return;
}
host.track('goal_cancel');
host.showStatus('Goal cancelled.');
host.showNotice('Goal cancelled.');
}

async function showGoalStatus(host: SlashCommandHost): Promise<void> {
Expand Down
13 changes: 13 additions & 0 deletions apps/kimi-code/src/tui/components/messages/goal-format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function formatGoalElapsed(ms: number): string {
const totalSeconds = Math.round(ms / 1000);
if (totalSeconds < 60) return `${String(totalSeconds)}s`;
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
if (minutes < 60) return `${String(minutes)}m ${seconds.toString().padStart(2, '0')}s`;
const hours = Math.floor(minutes / 60);
return `${String(hours)}h ${(minutes % 60).toString().padStart(2, '0')}m`;
}

export function pluralizeGoalCount(n: number, singular: string, plural?: string): string {
return `${String(n)} ${n === 1 ? singular : (plural ?? `${singular}s`)}`;
}
103 changes: 91 additions & 12 deletions apps/kimi-code/src/tui/components/messages/goal-markers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,43 @@
import type { GoalChange } from '@moonshot-ai/kimi-code-sdk';
import chalk from 'chalk';

import { STATUS_BULLET } from '#/tui/constant/symbols';
import type { ColorPalette } from '#/tui/theme/colors';

const HEAD_INDENT = ' ';
const DETAIL_INDENT = ' ';

type GoalMarkerActor = 'user' | 'model' | 'runtime' | 'system';

interface GoalMarkerOptions {
readonly marker?: string;
readonly textHex?: string;
readonly expandable?: boolean;
readonly indent?: string;
readonly leadingBlank?: boolean;
}

export class GoalMarkerComponent implements Component {
private expanded = false;
private readonly marker: string;
private readonly textHex: string;
private readonly expandable: boolean;
private readonly indent: string;
private readonly leadingBlank: boolean;

constructor(
private readonly headline: string,
private readonly detail: string | undefined,
private readonly colors: ColorPalette,
private readonly accentHex: string,
) {}
options: GoalMarkerOptions = {},
) {
this.marker = options.marker ?? '◦';
this.textHex = options.textHex ?? colors.textDim;
this.expandable = options.expandable ?? true;
this.indent = options.indent ?? HEAD_INDENT;
this.leadingBlank = options.leadingBlank ?? false;
}

invalidate(): void {}

Expand All @@ -33,20 +56,29 @@
}

render(width: number): string[] {
const dot = chalk.hex(this.accentHex)('◦');
const head = chalk.hex(this.colors.textDim)(this.headline);
const dot = chalk.hex(this.accentHex)(this.marker);
const head = chalk.hex(this.textHex)(this.headline);
const hasDetail = this.detail !== undefined && this.detail.length > 0;
if (!hasDetail) return [`${HEAD_INDENT}${dot} ${head}`];
if (!hasDetail) return this.withLeadingBlank([`${this.indent}${dot} ${head}`]);

if (!this.expandable) {
return this.withLeadingBlank([`${this.indent}${dot} ${head}`]);
}
if (!this.expanded) {
return [`${HEAD_INDENT}${dot} ${head} ${chalk.hex(this.colors.textMuted)('(ctrl+o)')}`];
return this.withLeadingBlank([
`${this.indent}${dot} ${head} ${chalk.hex(this.colors.textMuted)('(ctrl+o)')}`,
]);
}
const out = [`${HEAD_INDENT}${dot} ${head}`];
const out = [`${this.indent}${dot} ${head}`];
const wrapWidth = Math.max(20, width - DETAIL_INDENT.length);
for (const line of wrap(this.detail!, wrapWidth)) {
out.push(DETAIL_INDENT + chalk.hex(this.colors.textDim)(line));
}
return out;
return this.withLeadingBlank(out);
}

private withLeadingBlank(lines: string[]): string[] {
return this.leadingBlank ? ['', ...lines] : lines;
}
}

Expand All @@ -59,24 +91,37 @@
change: GoalChange,
colors: ColorPalette,
expanded: boolean,
actor?: GoalMarkerActor,
): GoalMarkerComponent | null {
const spec = markerSpec(change, colors);
const spec = markerSpec(change, colors, actor);
if (spec === null) return null;
const marker = new GoalMarkerComponent(spec.headline, change.reason, colors, spec.accentHex);
const marker = new GoalMarkerComponent(
spec.headline,
spec.detail ?? change.reason,
colors,
spec.accentHex,
spec.options,
);
marker.setExpanded(expanded);
return marker;
}

function markerSpec(
change: GoalChange,
colors: ColorPalette,
): { headline: string; accentHex: string } | null {
actor?: GoalMarkerActor,
): {
headline: string;
accentHex: string;
detail?: string | undefined;
options?: GoalMarkerOptions | undefined;
} | null {
if (change.kind === 'lifecycle') {
switch (change.status) {
case 'paused':
return { headline: 'Goal paused', accentHex: colors.textDim };
return prominentMarker(pausedHeadline(change.reason, actor), colors.warning);
case 'active':
return { headline: 'Goal resumed', accentHex: colors.primary };
return prominentMarker(resumedHeadline(actor), colors.primary);
case 'blocked':
// The system stopped pursuing the goal; resumable via `/goal resume`.
return { headline: 'Goal blocked', accentHex: colors.warning };
Expand All @@ -87,8 +132,42 @@
return null; // completion -> posts its own message, not a marker
}

function prominentMarker(headline: string, accentHex: string) {
return {
headline,
accentHex,
detail: undefined,
options: {
marker: STATUS_BULLET.trimEnd(),
textHex: accentHex,
expandable: false,
indent: '',
leadingBlank: true,
},
};
}

function pausedHeadline(reason: string | undefined, actor: GoalMarkerActor | undefined): string {
if (reason === 'Paused after interruption') return "Goal paused due to user's interruption";
if (actor === 'user') return 'Goal paused by the user.';
if (reason?.startsWith('Paused ') === true) return `Goal ${lowercaseFirst(reason)}`;
if (reason !== undefined && reason.length > 0) return `Goal paused: ${reason}`;
if (actor === 'model') return 'Goal paused by the agent.';
return 'Goal paused';
}

function resumedHeadline(actor: GoalMarkerActor | undefined): string {
if (actor === 'user') return 'Goal resumed by the user.';
if (actor === 'model') return 'Goal resumed by the agent.';
return 'Goal resumed';
}

function lowercaseFirst(text: string): string {
return text.length === 0 ? text : `${text[0]!.toLowerCase()}${text.slice(1)}`;
}

function wrap(text: string, width: number): string[] {
const words = text.replace(/\s+/g, ' ').trim().split(' ');

Check warning on line 170 in apps/kimi-code/src/tui/components/messages/goal-markers.ts

View workflow job for this annotation

GitHub Actions / lint

eslint-plugin-unicorn(prefer-string-replace-all)

Prefer `String#replaceAll()` over `String#replace()` when using a regex with the global flag.
const lines: string[] = [];
let current = '';
for (const word of words) {
Expand Down
15 changes: 3 additions & 12 deletions apps/kimi-code/src/tui/components/messages/goal-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { MESSAGE_INDENT } from '#/tui/constant/rendering';
import { STATUS_BULLET } from '#/tui/constant/symbols';
import type { ColorPalette } from '#/tui/theme/colors';
import { formatTokenCount } from '#/utils/usage/usage-format';
import { formatGoalElapsed } from './goal-format';
import { UsagePanelComponent } from './usage-panel';

const WRAP_WIDTH = 72;
Expand Down Expand Up @@ -161,7 +162,7 @@ export function buildGoalReportLines(options: GoalReportOptions): string[] {
),
);
}
lines.push(row('Running', value(formatElapsed(goal.wallClockMs))));
lines.push(row('Running', value(formatGoalElapsed(goal.wallClockMs))));
lines.push(row('Turns', value(`${goal.turnsUsed}`)));
lines.push(row('Tokens', value(formatTokenCount(goal.tokensUsed))));
if (!isComplete) {
Expand All @@ -186,7 +187,7 @@ function formatStopRow(goal: GoalSnapshot): string | null {
parts.push(`at ${formatTokenCount(budget.tokenBudget)} tokens`);
}
if (budget.wallClockBudgetMs !== null) {
parts.push(`after ${formatElapsed(budget.wallClockBudgetMs)}`);
parts.push(`after ${formatGoalElapsed(budget.wallClockBudgetMs)}`);
}
return parts.length > 0 ? parts.join(', ') : null;
}
Expand All @@ -204,16 +205,6 @@ function statusHex(status: GoalStatus, colors: ColorPalette): string {
}
}

function formatElapsed(ms: number): string {
const totalSeconds = Math.round(ms / 1000);
if (totalSeconds < 60) return `${totalSeconds}s`;
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
if (minutes < 60) return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
const hours = Math.floor(minutes / 60);
return `${hours}h ${(minutes % 60).toString().padStart(2, '0')}m`;
}

/** Word-wrap to `width`, capped at `maxLines` (last line gets an ellipsis when clipped). */
function wrap(text: string, width: number, maxLines: number): string[] {
const words = text.replaceAll(/\s+/g, ' ').trim().split(' ');
Expand Down
10 changes: 10 additions & 0 deletions apps/kimi-code/src/tui/components/messages/tool-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { agentSwarmResultSummaryFromOutput } from './agent-swarm-progress';
import { PlanBoxComponent } from './plan-box';
import { ShellExecutionComponent } from './shell-execution';
import { countNonEmptyLines, pickChip } from './tool-renderers/chip';
import { buildGoalToolHeader } from './tool-renderers/goal';
import { isGenericToolResult, pickResultRenderer } from './tool-renderers/registry';
import { TruncatedOutputComponent } from './tool-renderers/truncated';

Expand Down Expand Up @@ -1254,6 +1255,15 @@ export class ToolCallComponent extends Container {
return `${bullet}${tone.bold(label)}`;
}

const goalHeader = buildGoalToolHeader({
toolCall,
result,
colors,
bullet,
chip: isFinished && result !== undefined ? this.buildHeaderChip(result) : '',
});
if (goalHeader !== undefined) return goalHeader;

if (this.isSingleSubagentView()) {
return this.buildSingleSubagentHeader();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { computeDiffLines } from '#/tui/components/media/diff-preview';
import type { ToolCallBlockData, ToolResultBlockData } from '#/tui/types';

import { goalStatusChip } from './goal';
import { readMediaChip } from './media';
import { strArg } from './types';

Expand Down Expand Up @@ -110,6 +111,9 @@ const webSearchChip: ChipProvider = (_toolCall, result) => {
return pluralize(count, 'result');
};

const goalStatusOutputChip: ChipProvider = (_toolCall, result) =>
result.is_error ? '' : goalStatusChip(result.output);

const REGISTRY: Record<string, ChipProvider> = {
Edit: editChip,
Write: writeChip,
Expand All @@ -119,6 +123,8 @@ const REGISTRY: Record<string, ChipProvider> = {
Glob: globChip,
FetchURL: fetchChip,
WebSearch: webSearchChip,
CreateGoal: goalStatusOutputChip,
GetGoal: goalStatusOutputChip,
};

export function pickChip(toolName: string): ChipProvider | undefined {
Expand Down
Loading
Loading