diff --git a/.changeset/clickable-web-urls.md b/.changeset/clickable-web-urls.md new file mode 100644 index 0000000000..feba2e3932 --- /dev/null +++ b/.changeset/clickable-web-urls.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Make `kimi web` and `kimi web rotate-token` access URLs clickable in supported terminals. diff --git a/apps/kimi-code/src/cli/sub/web/rotate-token.ts b/apps/kimi-code/src/cli/sub/web/rotate-token.ts index eeaa63016b..fa2ca556ac 100644 --- a/apps/kimi-code/src/cli/sub/web/rotate-token.ts +++ b/apps/kimi-code/src/cli/sub/web/rotate-token.ts @@ -12,6 +12,7 @@ import type { Command } from 'commander'; import { darkColors } from '#/tui/theme/colors'; import { getDataDir } from '#/utils/paths'; +import { toTerminalHyperlink } from '#/utils/terminal-hyperlink'; import { accessUrlLines, splitTokenFragment } from './access-urls'; @@ -39,13 +40,15 @@ export function registerRotateTokenCommand(server: Command): void { const instance = await getLiveServerInstance(); if (instance !== undefined) { for (const { label, url: href } of accessUrlLines(instance.host, instance.port, token)) { - // De-emphasize the `#token=…` fragment so the host/port stands out. + // De-emphasize the `#token=…` fragment so the host/port stands out, + // and wrap the whole URL in an OSC 8 terminal hyperlink for + // click-to-open support in supported terminals. const [base, frag] = splitTokenFragment(href); - const rendered = + const display = frag === '' ? chalk.hex(darkColors.accent)(base) : chalk.hex(darkColors.accent)(base) + chalk.hex(darkColors.textDim)(frag); - process.stdout.write(` ${chalk.dim(label)}${rendered}\n`); + process.stdout.write(` ${chalk.dim(label)}${toTerminalHyperlink(display, href)}\n`); } } } catch (error) { diff --git a/apps/kimi-code/src/cli/sub/web/run.ts b/apps/kimi-code/src/cli/sub/web/run.ts index 26088c05fb..cddf78aa7b 100644 --- a/apps/kimi-code/src/cli/sub/web/run.ts +++ b/apps/kimi-code/src/cli/sub/web/run.ts @@ -21,6 +21,7 @@ import { getNativeWebAssetsDir } from '#/native/web-assets'; import { darkColors } from '#/tui/theme/colors'; import { openUrl as defaultOpenUrl } from '#/utils/open-url'; import { getDataDir } from '#/utils/paths'; +import { toTerminalHyperlink } from '#/utils/terminal-hyperlink'; import { initializeServerTelemetry } from '../../telemetry'; import { @@ -200,7 +201,8 @@ function formatReadyLine( const notice = dangerousBypassAuth ? `${formatDangerNoticeLines().join('\n')}\n` : ''; - return `${notice}Kimi server: ${buildOpenableUrl(origin, token)}\n`; + const href = buildOpenableUrl(origin, token); + return `${notice}Kimi server: ${toTerminalHyperlink(href, href)}\n`; } /** @@ -372,10 +374,12 @@ export function formatReadyBanner( const label = (text: string): string => chalk.bold.hex(darkColors.textDim)(text); const url = (text: string): string => chalk.hex(darkColors.accent)(text); // Render the `#token=…` fragment in a de-emphasized gray so the host/port - // stands out while the full URL stays selectable for copying. + // stands out while the full URL stays selectable for copying. Wrap the whole + // line in an OSC 8 terminal hyperlink so supported terminals can click to open. const urlWithDimToken = (href: string): string => { const [base, frag] = splitTokenFragment(href); - return frag === '' ? url(base) : url(base) + dim(frag); + const display = frag === '' ? url(base) : url(base) + dim(frag); + return toTerminalHyperlink(display, href); }; const port = Number(new URL(origin).port); diff --git a/apps/kimi-code/test/cli/web/web.test.ts b/apps/kimi-code/test/cli/web/web.test.ts index 1b51bfc535..969a517956 100644 --- a/apps/kimi-code/test/cli/web/web.test.ts +++ b/apps/kimi-code/test/cli/web/web.test.ts @@ -27,7 +27,9 @@ vi.mock('node:child_process', async (importOriginal) => { }); function stripAnsi(text: string): string { - return text.replaceAll(/\[[0-9;]*m/g, ''); + return text + .replaceAll(/\[[0-9;]*m/g, '') + .replaceAll(/]8;[^]*/g, ''); } function makeProgram(): Command {