Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
db8b9da
feat(agent-core-v2): add built-in capabilities (kimi-cu, kimi-webbrid…
wbxl2000 Jul 30, 2026
27e9ee1
fix(agent-core-v2): rename the webbridge wiring plugin to kimi-webbri…
wbxl2000 Jul 30, 2026
e9cd2e7
feat(agent-core-v2): shelf installs auto-complete capability binary l…
wbxl2000 Jul 30, 2026
9fcaf3f
fix(plugins): keep kimi-webbridge plugin version aligned with the ups…
wbxl2000 Jul 30, 2026
424485b
fix(agent-core-v2): never report the webbridge installer-script versi…
wbxl2000 Jul 30, 2026
859de74
chore(plugins): list kimi-cu on the marketplace without a pinned version
wbxl2000 Jul 30, 2026
38c8880
fix(agent-core-v2): fire onDidReload on plugin mutations, not just ex…
wbxl2000 Jul 30, 2026
431c1f1
feat(kap-server): add plugin management and marketplace REST routes
wbxl2000 Jul 30, 2026
1781895
feat(agent-core-v2): surface a machine-key note from capability installs
wbxl2000 Jul 30, 2026
262bdaa
feat(tui): let the real WebBridge marketplace entry win over the pinn…
wbxl2000 Jul 30, 2026
58b19b1
fix(tui): dim the installed state so it stops reading as the install …
wbxl2000 Jul 30, 2026
2d0b304
feat(agent-core-v2): converge plugin state across processes sharing a…
wbxl2000 Jul 30, 2026
49cd88e
fix(agent-core-v2): un-shadow webbridge user skills in BOTH user dirs
wbxl2000 Jul 30, 2026
4bdb2f6
feat(tui): show live runtime-setup progress for capability installs
wbxl2000 Jul 30, 2026
0635e99
docs(plugins): keep the kimi-cu marketplace blurb accurate for every …
wbxl2000 Jul 31, 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
5 changes: 5 additions & 0 deletions .changeset/built-in-capabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add built-in product capabilities (`kimi-cu`, `kimi-webbridge`) to the local server: a closed registry owns layered readiness detection and idempotent install orchestration — binary runtimes from fixed official CDN URLs plus agent wiring through the plugin service — exposed as `GET /api/v1/capabilities`, `GET /api/v1/capabilities/{id}`, and `POST /api/v1/capabilities/{id}:install` with client-polled progress. The plugin marketplace also gains an official `kimi-webbridge` entry (browser-control skills).
109 changes: 107 additions & 2 deletions apps/kimi-code/src/tui/commands/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { homedir as osHomedir } from 'node:os';
import { isAbsolute, join, resolve } from 'node:path';

import type { PluginInfo, PluginSummary } from '@moonshot-ai/kimi-code-sdk';
import type { CapabilityStatus, PluginInfo, PluginSummary } from '@moonshot-ai/kimi-code-sdk';

import {
PluginInstallTrustConfirmComponent,
Expand All @@ -26,7 +26,7 @@ import {
isOfficialPluginSource,
} from '../utils/plugin-source-label';
import { QUOTA_CONSUMING_PLUGIN_IDS } from '#/constant/app';
import { loadPluginMarketplace } from '#/utils/plugin-marketplace';
import { loadPluginMarketplace, type PluginMarketplaceEntry } from '#/utils/plugin-marketplace';
import { openUrl } from '#/utils/open-url';
import type { SlashCommandHost } from './dispatch';

Expand Down Expand Up @@ -299,6 +299,101 @@ async function confirmInstallTrust(
});
}

const CAPABILITY_POLL_INTERVAL_MS = 700;
const CAPABILITY_POLL_ATTEMPTS = 260; // ~3 minutes of runtime setup budget

/** Capability entries (kimi-cu, kimi-webbridge) install through the
* capability service — full runtime + wiring orchestration with live
* progress — instead of a bare plugin-package install. v1 engines have no
* capability surface, in which case every entry falls back to the plain
* plugin path. */
async function isCapabilityEntry(host: SlashCommandHost, id: string): Promise<boolean> {
try {
const capabilities = await host.requireSession().listCapabilities();
return capabilities.some((capability) => capability.id === id);
} catch {
return false;
}
}

/** Poll a background capability install, mirroring progress into the
* panel's inline installing line until it settles (or we run out of budget). */
async function pollCapabilityInstall(
host: SlashCommandHost,
panel: PluginsPanelComponent,
id: string,
label: string,
): Promise<CapabilityStatus | undefined> {
const session = host.requireSession();
for (let attempt = 0; attempt < CAPABILITY_POLL_ATTEMPTS; attempt += 1) {
await new Promise((resolve) => {
setTimeout(resolve, CAPABILITY_POLL_INTERVAL_MS);
});
const status = await session.getCapability(id);
if (!status.install.running) return status;
const step = status.install.step ?? 'configuring runtime';
const percent = status.install.percent;
panel.setInstalling(
`${truncateForStatus(label)} — ${step}${percent !== undefined ? ` ${percent}%` : ''}`,
);
host.state.ui.requestRender();
}
return undefined;
}

export const __pluginsCommandInternals = {
isCapabilityEntry,
pollCapabilityInstall,
removePlugin,
};

async function installCapabilityFromPanel(
host: SlashCommandHost,
panel: PluginsPanelComponent,
entry: PluginMarketplaceEntry,
): Promise<void> {
const label = entry.displayName;
// Capability entries are official by construction; the trust prompt is
// reserved for unreviewed third-party plugins.
panel.setInstalling(truncateForStatus(label));
host.state.ui.requestRender();
try {
await host.requireSession().installCapability(entry.id);
} catch (error) {
panel.clearInstalling();
host.state.ui.requestRender();
host.showError(`Failed to install ${label}: ${formatErrorMessage(error)}`);
host.restoreEditor();
return;
}
let result: CapabilityStatus | undefined;
try {
result = await pollCapabilityInstall(host, panel, entry.id, label);
} catch {
result = undefined;
}
panel.clearInstalling();
// Close the panel so the result lines land in the transcript, matching the
// plain plugin install flow.
host.restoreEditor();
if (result === undefined) {
host.showStatus(`${label} setup is still running in the background; /plugins shows its state.`);
return;
}
if (result.install.error !== undefined) {
host.showError(`${label} setup failed: ${result.install.error}. Install again from /plugins to retry.`);
return;
}
const migrationNote =
result.install.note === 'user-skill-migrated'
? ' Your manually installed skill is now managed as a plugin.'
: '';
host.showStatus(
`${label} is ready${result.version !== undefined ? ` (v${result.version})` : ''}.${migrationNote}`,
);
host.showStatus(PLUGIN_RELOAD_HINT, 'warning');
}

async function installFromPanel(
host: SlashCommandHost,
panel: PluginsPanelComponent,
Expand Down Expand Up @@ -400,6 +495,10 @@ async function handlePluginsPanelSelection(
await showPluginsPicker(host, { initialTab: 'installed' });
return;
case 'install':
if (await isCapabilityEntry(host, selection.entry.id)) {
await installCapabilityFromPanel(host, panel, selection.entry);
return;
}
await installFromPanel(
host,
panel,
Expand Down Expand Up @@ -454,6 +553,12 @@ async function handlePluginMcpSelection(
async function removePlugin(host: SlashCommandHost, id: string): Promise<void> {
await host.requireSession().removePlugin(id);
host.showStatus(`Removed ${id}.`);
if (await isCapabilityEntry(host, id)) {
// Capability runtimes (KimiCU.app / WebBridge daemon) are deliberately
// never uninstalled by plugin removal — say so, since the capability
// still works until the user removes those separately.
host.showStatus('Note: the runtime binaries were left untouched and the capability still works. Reinstall any time from the Official tab.');
}
host.showStatus(PLUGIN_RELOAD_HINT, 'warning');
}

Expand Down
59 changes: 35 additions & 24 deletions apps/kimi-code/src/tui/components/dialogs/plugins-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ const INSTALL_TRUST_EXIT = 'exit';
const INSTALL_TRUST_TRUST = 'trust';
const ELLIPSIS = '…';

// Hardcoded Web Bridge promotion: a built-in entry that always leads the
// Official tab, even when the marketplace catalog is unavailable. Selecting it
// opens the install page in the browser rather than installing from a source,
// because Web Bridge is a browser extension + daemon, not a plugin package.
// Hardcoded Web Bridge promotion: a built-in fallback shown only while the
// marketplace catalog is loading, unreachable, or predates the real
// `kimi-webbridge` entry. Selecting it opens the install page in the browser;
// once the catalog carries the real entry, that row wins and installs
// normally.
const WEB_BRIDGE_URL = 'https://www.kimi.com/features/webbridge#local-agent';
const WEB_BRIDGE_ENTRY: PluginMarketplaceEntry = {
id: 'kimi-webbridge',
Expand Down Expand Up @@ -284,10 +285,12 @@ function pluginStatus(plugin: PluginSummary): string | undefined {
}

function marketplaceStatusStyle(status: string, colors: ColorPalette): (text: string) => string {
// "update …" is a warning (actionable); "installed …" is success;
// "install …" is the available action.
// States recede, actions pop: "installed …" is a quiet fact (dim), while
// "install …" (the available action) stays primary and "update …" stays a
// warning — the two used to share near-identical green-ish treatments in
// the same column and read as interchangeable.
if (status.startsWith('update')) return chalk.hex(colors.warning);
if (status.startsWith('installed')) return chalk.hex(colors.success);
if (status.startsWith('installed')) return chalk.hex(colors.textDim);
return chalk.hex(colors.primary);
}

Expand Down Expand Up @@ -424,19 +427,16 @@ export class PluginsPanelComponent extends Container implements Focusable {
}

private get officialEntries(): readonly PluginMarketplaceEntry[] {
// The hardcoded Web Bridge entry always leads the Official tab, even when
// the catalog is loading or unreachable. Dedupe by id so a catalog that
// also lists it does not render a second row.
return [WEB_BRIDGE_ENTRY, ...this.officialCatalogEntries];
// The real catalog entry wins when present (it installs the actual
// plugin); the hardcoded promo row is only a fallback while the catalog
// is loading, unreachable, or predates it — never a duplicate row.
return this.officialCatalogEntries.some((entry) => entry.id === WEB_BRIDGE_ENTRY.id)
? this.officialCatalogEntries
: [WEB_BRIDGE_ENTRY, ...this.officialCatalogEntries];
}

private get officialCatalogEntries(): readonly PluginMarketplaceEntry[] {
// Dedupe by id (not reference): if the official catalog also lists
// kimi-webbridge, the pinned row already represents it, so suppress the
// catalog copy to avoid a duplicate row on the Official tab.
return this.marketplaceEntries.filter(
(entry) => entry.tier === 'official' && entry.id !== WEB_BRIDGE_ENTRY.id,
);
return this.marketplaceEntries.filter((entry) => entry.tier === 'official');
}

private get thirdPartyEntries(): readonly PluginMarketplaceEntry[] {
Expand Down Expand Up @@ -661,6 +661,10 @@ export class PluginsPanelComponent extends Container implements Focusable {
width: number,
entries: readonly PluginMarketplaceEntry[],
indexOffset = 0,
// Counts (installed/available footer) are computed over this list:
// the Official tab renders the pinned promo as a row but excludes it
// from the catalog counts, matching its pre-catalog semantics.
entriesForCount: readonly PluginMarketplaceEntry[] = entries,
): void {
const colors = currentTheme.palette;
if (this.market.status === 'loading' || this.market.status === 'idle') {
Expand All @@ -679,20 +683,27 @@ export class PluginsPanelComponent extends Container implements Focusable {
lines.push(...this.renderMarketplaceRow(entries[i]!, i + indexOffset, width));
}
}
const installedCount = entries.filter((e) => this.opts.installedIds.has(e.id)).length;
const installedCount = entriesForCount.filter((e) => this.opts.installedIds.has(e.id)).length;
lines.push('');
lines.push(
mutedHintLine(` ${installedCount} installed · ${entries.length - installedCount} available`, colors),
mutedHintLine(
` ${installedCount} installed · ${entriesForCount.length - installedCount} available`,
colors,
),
);
lines.push(mutedHintLine(` Source: ${this.market.source}`, colors));
}

private renderOfficial(lines: string[], width: number): void {
// Web Bridge is pinned above the catalog and stays visible while the
// catalog loads or errors, since it's built into the TUI rather than
// fetched. Catalog rows shift down by one index to match.
lines.push(...this.renderMarketplaceRow(WEB_BRIDGE_ENTRY, 0, width));
this.renderMarketplaceTab(lines, width, this.officialCatalogEntries, 1);
// Loading / error: catalog rows can't render yet — show only the pinned
// promo as the fallback. Once loaded, `officialEntries` already includes
// the promo only when the catalog lacks the real entry.
if (this.market.status !== 'loaded') {
lines.push(...this.renderMarketplaceRow(WEB_BRIDGE_ENTRY, 0, width));
this.renderMarketplaceTab(lines, width, [], 1);
return;
}
this.renderMarketplaceTab(lines, width, this.officialEntries, 0, this.officialCatalogEntries);
}

private renderThirdParty(lines: string[], width: number): void {
Expand Down
94 changes: 94 additions & 0 deletions apps/kimi-code/test/tui/commands/plugins-capability.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { __pluginsCommandInternals } from '#/tui/commands/plugins';

const { isCapabilityEntry, pollCapabilityInstall, removePlugin } = __pluginsCommandInternals;

function fakeHost(overrides: {
capabilities?: Array<{ id: string }>;
capabilityStatus?: () => Promise<{
install: { running: boolean; step?: string; percent?: number; error?: string };
}>;
}) {
const statuses: string[] = [];
const renders: number[] = [];
const session = {
listCapabilities: overrides.capabilities
? () => Promise.resolve(overrides.capabilities)
: () => Promise.reject(new Error('unavailable on this engine')),
getCapability:
overrides.capabilityStatus ??
(() => Promise.resolve({ install: { running: false } })),
removePlugin: () => Promise.resolve(),
};
const host = {
requireSession: () => session,
showStatus: (text: string) => {
statuses.push(text);
},
state: { ui: { requestRender: () => renders.push(1) } },
};
return { host: host as never, statuses, renders };
}

function fakePanel() {
const lines: (string | undefined)[] = [];
return {
panel: {
setInstalling: (label: string) => {
lines.push(label);
},
clearInstalling: () => {
lines.push(undefined);
},
} as never,
lines,
};
}

describe('plugins command capability surface', () => {
beforeEach(() => {
vi.restoreAllMocks();
});

it('detects capability entries and falls back when the engine lacks the surface', async () => {
const withCaps = fakeHost({ capabilities: [{ id: 'kimi-cu' }] });
expect(await isCapabilityEntry(withCaps.host, 'kimi-cu')).toBe(true);
expect(await isCapabilityEntry(withCaps.host, 'superpowers')).toBe(false);

const v1 = fakeHost({});
expect(await isCapabilityEntry(v1.host, 'kimi-cu')).toBe(false);
});

it('polls progress into the panel until the install settles', async () => {
let calls = 0;
const { host } = fakeHost({
capabilityStatus: () => {
calls += 1;
if (calls === 1) {
return Promise.resolve({ install: { running: true, step: 'download', percent: 40 } });
}
return Promise.resolve({ install: { running: false } });
},
});
const { panel, lines } = fakePanel();

const result = await pollCapabilityInstall(host, panel, 'kimi-cu', 'Kimi Computer Use');

expect(result?.install.running).toBe(false);
expect(lines).toContain('Kimi Computer Use — download 40%');
});

it('removePlugin notes that capability runtimes are left untouched', async () => {
const { host, statuses } = fakeHost({ capabilities: [{ id: 'kimi-cu' }] });
await removePlugin(host, 'kimi-cu');
expect(statuses.some((s) => s.includes('Removed kimi-cu'))).toBe(true);
expect(statuses.some((s) => s.includes('runtime binaries were left untouched'))).toBe(true);
});

it('removePlugin stays quiet for non-capability plugins', async () => {
const { host, statuses } = fakeHost({ capabilities: [{ id: 'kimi-cu' }] });
await removePlugin(host, 'superpowers');
expect(statuses.some((s) => s.includes('runtime binaries'))).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ describe('plugins selector dialogs', () => {
});
});

it('does not duplicate Web Bridge when the catalog also lists it', () => {
it('lets the real catalog entry win over the pinned Web Bridge promo', () => {
const entries = [
{
id: 'kimi-webbridge',
Expand All @@ -376,12 +376,18 @@ describe('plugins selector dialogs', () => {
},
...officialEntries,
];
const { panel } = makePanel({ initialTab: 'official' });
const { panel, onSelect } = makePanel({ initialTab: 'official' });
panel.setMarketplace(entries, '/tmp/marketplace.json');
const out = strip(renderRaw(panel));
// The label should appear exactly once — the hardcoded row wins, the
// catalog copy is filtered out.
// Exactly one row, and it is the installable catalog copy — the hardcoded
// open-in-browser promo is suppressed.
expect(out.split('Kimi WebBridge').length - 1).toBe(1);
expect(out).not.toContain('open in browser');
panel.handleInput('\r'); // index 0 → the real entry installs
expect(onSelect).toHaveBeenCalledWith({
kind: 'install',
entry: expect.objectContaining({ id: 'kimi-webbridge', source: 'https://x/w.zip' }),
});
});

it('installs a Third-party entry whose id matches the pinned WebBridge', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/scripts/check-domain-layers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ const DOMAIN_LAYER = new Map([
['permissionPolicy', 3],
['permissionRules', 3],
['plugin', 3],
['capability', 3],
['record', 3],
['modelCatalog', 3],
['agentProfileCatalog', 3],
Expand Down
Loading