diff --git a/.changeset/webbridge-install-next-steps.md b/.changeset/webbridge-install-next-steps.md new file mode 100644 index 000000000..aadce61d1 --- /dev/null +++ b/.changeset/webbridge-install-next-steps.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Show browser extension links and activation steps after installing Kimi WebBridge. diff --git a/apps/kimi-code/src/tui/commands/plugins.ts b/apps/kimi-code/src/tui/commands/plugins.ts index d2d97a4c6..439ff658a 100644 --- a/apps/kimi-code/src/tui/commands/plugins.ts +++ b/apps/kimi-code/src/tui/commands/plugins.ts @@ -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( diff --git a/apps/kimi-code/test/tui/commands/plugins-capability.test.ts b/apps/kimi-code/test/tui/commands/plugins-capability.test.ts index 7fc7dbaee..a63f8a343 100644 --- a/apps/kimi-code/test/tui/commands/plugins-capability.test.ts +++ b/apps/kimi-code/test/tui/commands/plugins-capability.test.ts @@ -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,