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

Show the underlying error when migration fails.
51 changes: 48 additions & 3 deletions apps/kimi-code/src/migration/migration-screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export class MigrationScreenComponent extends Container implements Focusable {
private spinnerTimer: ReturnType<typeof setInterval> | undefined;
private report: MigrationReport | undefined;
private migrationFailed = false;
private migrationFailureReason: string | undefined;

constructor(opts: MigrationScreenOptions) {
super();
Expand All @@ -113,8 +114,9 @@ export class MigrationScreenComponent extends Container implements Focusable {
}

/** Host calls this if runMigration threw. */
showFailure(): void {
showFailure(error?: unknown): void {
this.migrationFailed = true;
this.migrationFailureReason = formatMigrationFailureReason(error);
this.phase = 'result';
this.stopSpinner();
}
Expand Down Expand Up @@ -260,8 +262,8 @@ export class MigrationScreenComponent extends Container implements Focusable {
this.showResult(report);
this.opts.requestRender?.();
},
() => {
this.showFailure();
(error) => {
this.showFailure(error);
this.opts.requestRender?.();
},
);
Expand All @@ -280,6 +282,10 @@ export class MigrationScreenComponent extends Container implements Focusable {
const lines: string[] = [chalk.hex(colors.primary)('─'.repeat(width))];
if (this.migrationFailed) {
lines.push(chalk.hex(colors.error).bold(' Migration failed'));
if (this.migrationFailureReason !== undefined) {
lines.push('');
lines.push(chalk.hex(colors.text)(` Reason: ${this.migrationFailureReason}`));
}
lines.push('');
lines.push(chalk.hex(colors.text)(' You can retry later by running "kimi migrate".'));
lines.push('');
Expand Down Expand Up @@ -487,6 +493,45 @@ export class MigrationScreenComponent extends Container implements Focusable {
}
}

function formatMigrationFailureReason(error: unknown): string | undefined {
let reason: string | undefined;
if (error instanceof Error) {
reason = error.message !== '' ? error.message : error.name;
} else if (typeof error === 'string') {
reason = error;
} else if (typeof error === 'object' && error !== null) {
const maybeMessage = (error as { readonly message?: unknown }).message;
if (typeof maybeMessage === 'string' && maybeMessage !== '') {
reason = maybeMessage;
}
}
if (reason === undefined) {
switch (typeof error) {
case 'number':
case 'boolean':
case 'bigint':
reason = `${error}`;
break;
case 'symbol':
reason =
error.description !== undefined ? `Symbol(${error.description})` : 'Symbol rejection';
break;
case 'function':
reason = error.name !== '' ? `Function ${error.name}` : 'Function rejection';
break;
case 'object':
if (error !== null) reason = 'Object rejection';
break;
case 'undefined':
break;
case 'string':
break;
}
}
const trimmed = reason?.trim();
return trimmed === undefined || trimmed === '' ? undefined : trimmed;
}

function summarizePlan(plan: MigrationPlan): string {
const parts: string[] = [];
if (plan.totalSessions > 0) parts.push(`${plan.totalSessions} sessions`);
Expand Down
6 changes: 4 additions & 2 deletions apps/kimi-code/test/migration/migration-screen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ describe('MigrationScreenComponent — execution wiring', () => {
expect(onCompleteResult?.migrated).toBe(true);
});

it('lands on the failure screen when the runner rejects', async () => {
it('lands on the failure screen with the runner rejection reason', async () => {
const c = new MigrationScreenComponent({
plan: makePlan(),
sourceHome: '/x/.kimi',
Expand All @@ -592,6 +592,8 @@ describe('MigrationScreenComponent — execution wiring', () => {
c.handleInput('\r'); // ask1: Migrate now
c.handleInput('\r'); // ask2: Config only -> begins migration
await new Promise((res) => setTimeout(res, 0));
expect(c.render(80).join('\n')).toContain('Migration failed');
const out = c.render(80).join('\n');
expect(out).toContain('Migration failed');
expect(out).toContain('Reason: boom');
});
});