fix(tui): surface capability install errors (#2682)
Some checks are pending
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
CI / test (3) (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions

This commit is contained in:
qer 2026-08-06 14:02:59 +08:00 committed by GitHub
parent 3c75a27da6
commit 013203421d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 83 additions and 6 deletions

View file

@ -2,4 +2,4 @@
"@moonshot-ai/kimi-code": minor
---
Add Windows support for the built-in Kimi Computer Use capability. Install it from `/plugins` on Windows x64.
Add Windows support for the built-in Kimi Computer Use capability and show the underlying error when capability setup fails. Install it from `/plugins` on Windows x64.

View file

@ -539,7 +539,8 @@ async function installCapabilityFromPanel(
}
logCapabilityStatus(result);
if (result.install.error !== undefined) {
host.showError(`${label} installation failed. Check the logs and install again from /plugins.`);
host.showError(`${label} installation failed: ${result.install.error}`);
host.showStatus('Fix the reported error, then install again from /plugins.', 'warning');
return;
}
if (result.state !== 'ready') {

View file

@ -199,6 +199,29 @@ describe('plugins command capability surface', () => {
expect(statuses.some((s) => s.includes('is installed'))).toBe(true);
});
it('shows the engine error when a background capability install fails', async () => {
const { host, statuses } = fakeHost({
engineV2: true,
capabilityStatus: () =>
Promise.resolve({
state: 'not_installed',
steps: [],
install: { running: false, error: 'Authenticode signature is not valid' },
}),
});
await installCapabilityFromPanel(
host,
fakePanel().panel,
{ id: 'kimi-cu', displayName: 'Kimi Computer Use', source: 'capability:kimi-cu' } as never,
);
expect(statuses).toContain(
'Kimi Computer Use installation failed: Authenticode signature is not valid',
);
expect(statuses).toContain('Fix the reported error, then install again from /plugins.');
});
it('shows required permissions once after installation instead of exposing step details', async () => {
const { host, statuses } = fakeHost({
engineV2: true,

View file

@ -4,14 +4,15 @@
* Holds the closed registry of built-in capability entries and serializes
* install runs per entry. Install progress lives in memory only and is
* polled by clients; a failed attempt leaves its error in the progress state
* until the next attempt starts. Listing degrades a single entry's failing
* detection to a failed step on that entry instead of rejecting the whole
* list. Bound at App scope.
* until the next attempt starts and logs the failure through `log`. Listing
* degrades a single entry's failing detection to a failed step on that entry
* instead of rejecting the whole list. Bound at App scope.
*/
import { homedir } from 'node:os';
import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { ILogService } from '#/_base/log/log';
import { Error2 } from '#/errors';
import { IBootstrapService } from '#/app/bootstrap/bootstrap';
import { IPluginService } from '#/app/plugin/plugin';
@ -42,6 +43,7 @@ export class CapabilityService implements ICapabilityService {
@IBootstrapService bootstrap: IBootstrapService,
@IPluginService plugins: IPluginService,
@IHostProcessService hostProcess: IHostProcessService,
@ILogService private readonly log: ILogService,
entriesOverride?: readonly CapabilityEntry[],
) {
if (entriesOverride !== undefined) {
@ -98,6 +100,12 @@ export class CapabilityService implements ICapabilityService {
});
this.installProgress.set(entry.id, { running: false });
} catch (error) {
const step = this.installProgress.get(entry.id)?.step;
this.log.warn('capability install failed', {
capabilityId: entry.id,
step,
error,
});
this.installProgress.set(entry.id, {
running: false,
error: error instanceof Error ? error.message : String(error),

View file

@ -7,6 +7,7 @@
import { describe, expect, it } from 'vitest';
import { isError2 } from '#/_base/errors/errors';
import type { ILogService, LogPayload } from '#/_base/log/log';
import { CapabilityErrors } from '#/app/capability/errors';
import { CapabilityService } from '#/app/capability/capabilityService';
import type {
@ -15,6 +16,8 @@ import type {
CapabilityInstallReporter,
} from '#/app/capability/types';
import { stubLog } from '../../_base/log/stubs';
function fakeEntry(overrides: {
id: 'kimi-cu' | 'kimi-webbridge';
pluginId?: string;
@ -36,12 +39,16 @@ function fakeEntry(overrides: {
};
}
function fakeService(entries: readonly CapabilityEntry[]): CapabilityService {
function fakeService(
entries: readonly CapabilityEntry[],
log: ILogService = stubLog(),
): CapabilityService {
// bootstrap / hostProcess are unused when entries are injected.
return new CapabilityService(
undefined as never,
undefined as never,
undefined as never,
log,
entries,
);
}
@ -239,4 +246,42 @@ describe('CapabilityService', () => {
expect(retried.install.error).toBeUndefined();
expect(attempts).toBe(2);
});
it('logs an install error with its last progress step when setup fails', async () => {
const warnings: Array<{ message: string; payload?: LogPayload }> = [];
let resolveLogged: (() => void) | undefined;
const logged = new Promise<void>((resolve) => {
resolveLogged = resolve;
});
const error = new Error('signature mismatch');
const log = {
...stubLog(),
warn: (message: string, payload?: LogPayload) => {
warnings.push({ message, payload });
resolveLogged?.();
},
} satisfies ILogService;
const service = fakeService(
[
fakeEntry({
id: 'kimi-cu',
install: async (report) => {
report('runtime');
throw error;
},
}),
],
log,
);
await service.installCapability('kimi-cu');
await logged;
expect(warnings).toEqual([
{
message: 'capability install failed',
payload: { capabilityId: 'kimi-cu', step: 'runtime', error },
},
]);
});
});