fix(tui): show WebBridge setup steps after install (#2692)

* fix(tui): show WebBridge setup steps after install

* fix(tui): scope WebBridge hint to capability install

* fix(tui): format WebBridge setup links

* fix(tui): align WebBridge setup with live reload

* fix(tui): retain WebBridge session activation step

* fix(tui): compact WebBridge setup links

* fix(tui): list WebBridge setup links clearly

* fix(tui): show clickable WebBridge URLs

* Revert "fix(tui): show clickable WebBridge URLs"

This reverts commit 373ffae4b8cf4256131002b990f9a1869e9ee156.

* fix(tui): restore WebBridge setup heading
This commit is contained in:
qer 2026-08-06 20:19:32 +08:00 committed by GitHub
parent 335588e259
commit 03aa66ca0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 144 additions and 3 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Show browser extension links and activation steps after installing Kimi WebBridge.

View file

@ -8,6 +8,7 @@ import {
type PluginSummary,
type Session,
} from '@moonshot-ai/kimi-code-sdk';
import { Markdown, Spacer } from '@moonshot-ai/pi-tui';
import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui';
import {
@ -26,6 +27,7 @@ import {
buildPluginsListLines,
} from '../components/messages/plugins-status-panel';
import { UsagePanelComponent } from '../components/messages/usage-panel';
import { createMarkdownTheme } from '../theme/pi-tui-theme';
import { formatErrorMessage } from '../utils/event-payload';
import {
formatPluginSourceLabel,
@ -561,6 +563,15 @@ async function installCapabilityFromPanel(
host.showStatus(pluginReloadHint(), 'warning');
return;
}
if (entry.id === 'kimi-webbridge') {
host.showNotice(`${label} is installed.`);
host.state.transcriptContainer.addChild(new Spacer(1));
host.state.transcriptContainer.addChild(
new Markdown(WEBBRIDGE_POST_INSTALL_MARKDOWN, 2, 0, createMarkdownTheme()),
);
host.state.ui.requestRender();
return;
}
host.showStatus(`${label} is installed.`);
host.showStatus(pluginReloadHint(), 'warning');
}
@ -778,6 +789,17 @@ function pluginReloadHint(): string {
return isKimiV2Enabled() ? PLUGIN_RELOAD_HINT_V2 : PLUGIN_RELOAD_HINT;
}
const WEBBRIDGE_POST_INSTALL_MARKDOWN = [
'*Two steps left to use Kimi WebBridge:*',
'1. Install the browser extension',
'',
' - [Chrome Web Store](https://chromewebstore.google.com/detail/kimi-webbridge/fldmhceldgbpfpkbgopacenieobmligc)',
' - [Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/kimi-webbridge/bnlffdbcfnanfbknnlaflhlhkocccckg)',
' - [Manual installation guide](https://www.kimi.com/code/docs/kimi-code-cli/customization/plugins.html#install-the-browser-extension)',
'',
'2. Run `/reload` or `/new` to apply it.',
].join('\n');
const PLUGIN_QUOTA_NOTE = 'Note: This plugin consumes your quota.';
function showPluginInstallResult(

View file

@ -1,7 +1,9 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { log } from '@moonshot-ai/kimi-code-sdk';
import { resetCapabilitiesCache, setCapabilities, type Component } from '@moonshot-ai/pi-tui';
import { __pluginsCommandInternals } from '#/tui/commands/plugins';
import { NoticeMessageComponent } from '#/tui/components/messages/status-message';
const {
isCapabilityEntry,
@ -20,7 +22,9 @@ function fakeHost(overrides: {
}>;
}) {
const statuses: string[] = [];
const notices: { title: string; detail?: string }[] = [];
const renders: number[] = [];
const transcriptEntries: Component[] = [];
const installCapability = vi.fn(() => Promise.resolve());
const getCapability =
overrides.capabilityStatus ??
@ -46,10 +50,19 @@ function fakeHost(overrides: {
showError: (text: string) => {
statuses.push(text);
},
showNotice: (title: string, detail?: string) => {
notices.push({ title, detail });
transcriptEntries.push(new NoticeMessageComponent(title, detail));
},
restoreEditor: () => undefined,
state: { ui: { requestRender: () => renders.push(1) } },
state: {
ui: { requestRender: () => renders.push(1) },
transcriptContainer: {
addChild: (entry: Component) => transcriptEntries.push(entry),
},
},
};
return { host: host as never, statuses, renders, installCapability };
return { host: host as never, statuses, notices, renders, transcriptEntries, installCapability };
}
function fakePanel() {
@ -67,6 +80,23 @@ function fakePanel() {
};
}
function visibleLines(entries: readonly Component[], width = 100): string[] {
return entries
.flatMap((entry) => entry.render(width))
.map((line) =>
line
.replaceAll(/\u001B]8;;[^\u001B]*\u001B\\/g, '')
.replaceAll(/\u001B\[[0-9;]*m/g, '')
.trimEnd(),
);
}
function unwrappedVisibleText(entries: readonly Component[]): string {
return visibleLines(entries)
.join('\n')
.replaceAll(/\s+/g, '');
}
describe('plugins command capability surface', () => {
beforeEach(() => {
vi.restoreAllMocks();
@ -74,6 +104,10 @@ describe('plugins command capability surface', () => {
vi.spyOn(log, 'warn').mockImplementation(() => undefined);
});
afterEach(() => {
resetCapabilitiesCache();
});
it('routes built-in entries through capabilities only on v2', () => {
const v2 = fakeHost({ engineV2: true });
expect(
@ -201,6 +235,86 @@ describe('plugins command capability surface', () => {
expect(statuses.some((s) => s.includes('is installed'))).toBe(true);
});
it('renders visible clickable store URLs after WebBridge installs in a hyperlink-capable terminal', async () => {
setCapabilities({ images: null, trueColor: true, hyperlinks: true });
const { host, statuses, notices, transcriptEntries } = fakeHost({ engineV2: true });
await installCapabilityFromPanel(
host,
fakePanel().panel,
{
id: 'kimi-webbridge',
displayName: 'Kimi WebBridge',
source: 'capability:kimi-webbridge',
} as never,
);
expect(notices).toContainEqual({ title: 'Kimi WebBridge is installed.', detail: undefined });
expect(statuses).not.toContain('Run /new or /reload to apply plugin changes.');
const rendered = transcriptEntries.flatMap((entry) => entry.render(100)).join('\n');
expect(rendered).toContain(
'\u001B]8;;https://chromewebstore.google.com/detail/kimi-webbridge/fldmhceldgbpfpkbgopacenieobmligc\u001B\\',
);
expect(rendered).toContain('Chrome Web Store');
expect(rendered).toContain('Edge Add-ons');
expect(rendered).toContain('Manual installation guide');
expect(rendered).toContain('/reload');
expect(rendered).toContain('/new');
});
it('renders full store URLs after WebBridge installs in a terminal without hyperlinks', async () => {
setCapabilities({ images: null, trueColor: true, hyperlinks: false });
const { host, transcriptEntries } = fakeHost({ engineV2: true });
await installCapabilityFromPanel(
host,
fakePanel().panel,
{
id: 'kimi-webbridge',
displayName: 'Kimi WebBridge',
source: 'capability:kimi-webbridge',
} as never,
);
const rendered = transcriptEntries.flatMap((entry) => entry.render(100)).join('\n');
expect(rendered).not.toContain('\u001B]8;;');
expect(unwrappedVisibleText(transcriptEntries)).toContain(
'https://chromewebstore.google.com/detail/kimi-webbridge/fldmhceldgbpfpkbgopacenieobmligc',
);
});
it('separates the WebBridge install result from its setup steps with one blank line', async () => {
setCapabilities({ images: null, trueColor: true, hyperlinks: true });
const { host, transcriptEntries } = fakeHost({ engineV2: true });
await installCapabilityFromPanel(
host,
fakePanel().panel,
{
id: 'kimi-webbridge',
displayName: 'Kimi WebBridge',
source: 'capability:kimi-webbridge',
} as never,
);
const lines = visibleLines(transcriptEntries, 180);
const installed = lines.findIndex((line) => line.includes('Kimi WebBridge is installed.'));
const intro = lines.findIndex((line) =>
line.includes('Two steps left to use Kimi WebBridge:'),
);
const firstStep = lines.findIndex((line) =>
line.includes('Install the browser extension'),
);
const secondStep = lines.findIndex((line) => line.includes('Run /reload or /new to apply it.'));
expect(lines.slice(installed + 1, intro)).toEqual(['']);
expect(firstStep).toBe(intro + 1);
expect(lines[firstStep]).toContain('1.');
expect(lines.find((line) => line.includes('Chrome Web Store'))).toContain('•');
expect(lines.find((line) => line.includes('Edge Add-ons'))).toContain('•');
expect(lines.find((line) => line.includes('Manual installation guide'))).toContain('•');
expect(lines[secondStep]).toContain('2.');
});
it('shows the engine error when a background capability install fails', async () => {
const { host, statuses } = fakeHost({
engineV2: true,