diff --git a/.changeset/refresh-web-icons.md b/.changeset/refresh-web-icons.md
new file mode 100644
index 000000000..6d6d022af
--- /dev/null
+++ b/.changeset/refresh-web-icons.md
@@ -0,0 +1,5 @@
+---
+"@moonshot-ai/kimi-code": patch
+---
+
+Refresh the web UI icon set and unify the message copy and undo button hover states and tooltips.
diff --git a/apps/kimi-web/design/design-system.html b/apps/kimi-web/design/design-system.html
index 261b678d4..9ca0de3b8 100644
--- a/apps/kimi-web/design/design-system.html
+++ b/apps/kimi-web/design/design-system.html
@@ -1039,79 +1039,82 @@
图标 Icon
- 图标一律从集中注册表 lib/icons.ts 取用:模板中用 <Icon name size /> 组件(components/ui/Icon.vue),v-html 场景(如工具 glyph)用 iconSvg(name, size)。禁止手写 <svg>——scripts/check-style.mjs 的 icon-from-registry 规则会对游离 SVG 告警。默认线稿风格:描边 1.5px、stroke="currentColor"、stroke-linecap/linejoin: round、无 fill,颜色跟随文字;尺寸走下方三档 token。少数「实心即语义」的图标(play / stop / star)为 fill 例外。
+ 图标一律从集中注册表 lib/icons.ts 取用:模板中用 <Icon name size /> 组件(components/ui/Icon.vue),v-html 场景(如工具 glyph)用 iconSvg(name, size)。禁止手写 <svg>——scripts/check-style.mjs 的 icon-from-registry 规则会对游离 SVG 告警。图标来自 Remix Icon(Apache-2.0),统一为 fill 风格(fill="currentColor"、24×24 源网格),颜色跟随文字;尺寸走下方三档 token。注册表由 scripts/gen-icon-data.mjs 从 @iconify-json/ri 按需生成,离线且 tree-shaken。
尺寸阶梯 Size scale
图标库 Icon library
- 当前注册的图标(按用途分组)。新增图标时,把图形以 name → body 加入 lib/icons.ts,按需补 viewBox(默认 16×16)/fill,再用 node --experimental-strip-types scripts/gen-icon-catalog.mjs 重新生成本目录,保证注册表与文档不漂移。
+ 当前注册的图标(按用途分组)。图标数据由 scripts/gen-icon-data.mjs 中的 NAME_TO_REMIX 映射决定,运行 pnpm gen:icons 生成 lib/icon-data.ts;本目录由 node --experimental-strip-types scripts/gen-icon-catalog.mjs 重新生成,保证注册表与文档不漂移。
-
Actions
-
-
-
-
-
-
-
-
-
-
-
-
-
sliders
-
-
Navigation & layout
-
-
-
-
-
-
-
-
-
-
-
-
Files & tools
-
-
-
-
-
-
file-text
-
-
-
-
-
-
-
-
-
-
-
-
Communication
-
-
-
-
Status & media
-
info
-
-
-
-
-
-
stop
-
-
-
dots-vertical
-
dots-horizontal
+
Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Navigation & layout
+
+
+
+
+
+
+
+
+
+
+
+
Files & tools
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Communication
+
+
+
+
Status & media
+
+
+
+
+
+
+
+
+
+
+
Other
+
+
禁止用 emoji 充当功能图标(唯一例外:月相 🌑…🌘,仅用于「等待 Agent 响应」聊天态)。Kimi 品牌标记(32×22 的眼睛 logo)属于品牌资产,不在此线稿图标体系内。
diff --git a/apps/kimi-web/package.json b/apps/kimi-web/package.json
index 9d60122f7..1b2eb5f2b 100644
--- a/apps/kimi-web/package.json
+++ b/apps/kimi-web/package.json
@@ -10,11 +10,15 @@
"build": "vite build",
"typecheck": "vue-tsc --noEmit",
"test": "vitest run",
- "check:style": "node scripts/check-style.mjs"
+ "check:style": "node scripts/check-style.mjs",
+ "gen:icons": "node scripts/gen-icon-data.mjs"
},
"dependencies": {
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
+ "@iconify-json/ri": "^1.2.10",
+ "@iconify/utils": "^3.1.3",
+ "@iconify/vue": "^5.0.1",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"katex": "^0.17.0",
diff --git a/apps/kimi-web/public/design-system.html b/apps/kimi-web/public/design-system.html
index 8425e98cd..96da55a35 100644
--- a/apps/kimi-web/public/design-system.html
+++ b/apps/kimi-web/public/design-system.html
@@ -1038,79 +1038,82 @@
图标 Icon
- 图标一律从集中注册表 lib/icons.ts 取用:模板中用 <Icon name size /> 组件(components/ui/Icon.vue),v-html 场景(如工具 glyph)用 iconSvg(name, size)。禁止手写 <svg>——scripts/check-style.mjs 的 icon-from-registry 规则会对游离 SVG 告警。默认线稿风格:描边 1.5px、stroke="currentColor"、stroke-linecap/linejoin: round、无 fill,颜色跟随文字;尺寸走下方三档 token。少数「实心即语义」的图标(play / stop / star)为 fill 例外。
+ 图标一律从集中注册表 lib/icons.ts 取用:模板中用 <Icon name size /> 组件(components/ui/Icon.vue),v-html 场景(如工具 glyph)用 iconSvg(name, size)。禁止手写 <svg>——scripts/check-style.mjs 的 icon-from-registry 规则会对游离 SVG 告警。图标来自 Remix Icon(Apache-2.0),统一为 fill 风格(fill="currentColor"、24×24 源网格),颜色跟随文字;尺寸走下方三档 token。注册表由 scripts/gen-icon-data.mjs 从 @iconify-json/ri 按需生成,离线且 tree-shaken。
尺寸阶梯 Size scale
图标库 Icon library
- 当前注册的图标(按用途分组)。新增图标时,把图形以 name → body 加入 lib/icons.ts,按需补 viewBox(默认 16×16)/fill,再用 node --experimental-strip-types scripts/gen-icon-catalog.mjs 重新生成本目录,保证注册表与文档不漂移。
+ 当前注册的图标(按用途分组)。图标数据由 scripts/gen-icon-data.mjs 中的 NAME_TO_REMIX 映射决定,运行 pnpm gen:icons 生成 lib/icon-data.ts;本目录由 node --experimental-strip-types scripts/gen-icon-catalog.mjs 重新生成,保证注册表与文档不漂移。
-
Actions
-
-
-
-
-
-
-
-
-
-
-
-
-
sliders
-
-
Navigation & layout
-
-
-
-
-
-
-
-
-
-
-
-
Files & tools
-
-
-
-
-
-
file-text
-
-
-
-
-
-
-
-
-
-
-
-
Communication
-
-
-
-
Status & media
-
info
-
-
-
-
-
-
stop
-
-
-
dots-vertical
-
dots-horizontal
+
Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Navigation & layout
+
+
+
+
+
+
+
+
+
+
+
+
Files & tools
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Communication
+
+
+
+
Status & media
+
+
+
+
+
+
+
+
+
+
+
Other
+
+
禁止用 emoji 充当功能图标(唯一例外:月相 🌑…🌘,仅用于「等待 Agent 响应」聊天态)。Kimi 品牌标记(32×22 的眼睛 logo)属于品牌资产,不在此线稿图标体系内。
diff --git a/apps/kimi-web/scripts/gen-icon-catalog.mjs b/apps/kimi-web/scripts/gen-icon-catalog.mjs
index b4f6a33a1..79063bf06 100644
--- a/apps/kimi-web/scripts/gen-icon-catalog.mjs
+++ b/apps/kimi-web/scripts/gen-icon-catalog.mjs
@@ -1,24 +1,21 @@
// scripts/gen-icon-catalog.mjs — generate the design-system §02 icon catalog
// HTML from the canonical registry (lib/icons.ts) so the two can never drift.
// Run: node --experimental-strip-types scripts/gen-icon-catalog.mjs
-import { ICONS } from '../src/lib/icons.ts';
+import { ICON_DATA } from '../src/lib/icon-data.ts';
// Display order + grouping. Names not listed here are appended under "Other".
const GROUPS = [
- ['Actions', ['plus', 'close', 'check', 'search', 'copy', 'link', 'external-link', 'download', 'undo', 'send', 'image', 'settings', 'sliders', 'log-in']],
+ ['Actions', ['plus', 'chat-new', 'close', 'check', 'search', 'copy', 'link', 'external-link', 'download', 'undo', 'send', 'image', 'settings', 'sliders', 'log-in']],
['Navigation & layout', ['chevron-down', 'chevron-right', 'arrow-up', 'arrow-down', 'arrow-right', 'minus', 'panel-collapse', 'panel-expand', 'expand', 'collapse', 'list']],
['Files & tools', ['folder', 'folder-closed', 'folder-plus', 'folder-solid', 'file', 'file-text', 'file-plus', 'file-off', 'image-off', 'code', 'terminal', 'pencil', 'glob', 'globe', 'check-list', 'bolt', 'git-pull-request']],
['Communication', ['message', 'mail', 'user']],
- ['Status & media', ['info', 'help-circle', 'alert-triangle', 'clock', 'sparkles', 'play', 'stop', 'star', 'star-outline', 'dots-vertical', 'dots-horizontal']],
+ ['Status & media', ['info', 'help-circle', 'alert-triangle', 'clock', 'sparkles', 'play', 'stop', 'star', 'star-outline', 'dots-horizontal']],
];
function render(name) {
- const def = ICONS[name];
- const vb = def.viewBox ?? '0 0 16 16';
- const fillAttrs = def.fill
- ? 'fill="currentColor"'
- : 'fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"';
- return ``;
+ const d = ICON_DATA[name];
+ const vb = `0 0 ${d.width ?? 24} ${d.height ?? 24}`;
+ return ``;
}
const seen = new Set();
@@ -31,7 +28,7 @@ for (const [label, names] of GROUPS) {
lines.push(` ${render(name)}${name}
`);
}
}
-const rest = Object.keys(ICONS).filter((n) => !seen.has(n));
+const rest = Object.keys(ICON_DATA).filter((n) => !seen.has(n));
if (rest.length) {
lines.push(' Other
');
for (const name of rest) {
diff --git a/apps/kimi-web/scripts/gen-icon-data.mjs b/apps/kimi-web/scripts/gen-icon-data.mjs
new file mode 100644
index 000000000..49ed7dc45
--- /dev/null
+++ b/apps/kimi-web/scripts/gen-icon-data.mjs
@@ -0,0 +1,155 @@
+#!/usr/bin/env node
+// scripts/gen-icon-data.mjs — generate src/lib/icon-data.ts, the tree-shaken
+// Remix Icon (ri) subset that backs apps/kimi-web's icon registry.
+//
+// Single source of truth for "which existing icon name maps to which Remix
+// icon" is the NAME_TO_REMIX map below. The SVG bytes are pulled straight from
+// @iconify-json/ri at generation time, so the registry stays library-sourced
+// (no hand-copied SVG) yet fully offline and tree-shaken (only the icons we
+// list here end up in the bundle).
+//
+// Run after changing the map: pnpm gen:icons
+
+import { writeFileSync } from 'node:fs';
+import { resolve, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import ri from '@iconify-json/ri/icons.json' with { type: 'json' };
+import { getIconData } from '@iconify/utils';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const OUT = resolve(__dirname, '../src/lib/icon-data.ts');
+
+// Existing icon name → Remix icon name (ri:, prefix omitted here).
+// Keep keys sorted within their group; the generated IconName union follows
+// this order. Every value must exist in @iconify-json/ri (validated below).
+const GROUPS = [
+ ['Actions', {
+ plus: 'add-line',
+ 'chat-new': 'chat-new-line',
+ close: 'close-line',
+ check: 'check-line',
+ search: 'search-line',
+ copy: 'file-copy-line',
+ link: 'links-line',
+ 'external-link': 'external-link-line',
+ download: 'download-line',
+ undo: 'arrow-go-back-line',
+ send: 'send-plane-line',
+ image: 'image-line',
+ settings: 'settings-3-line',
+ sliders: 'equalizer-line',
+ 'log-in': 'login-box-line',
+ }],
+ ['Navigation & layout', {
+ 'chevron-down': 'arrow-down-s-line',
+ 'chevron-right': 'arrow-right-s-line',
+ 'arrow-up': 'arrow-up-line',
+ 'arrow-down': 'arrow-down-line',
+ 'arrow-right': 'arrow-right-line',
+ minus: 'subtract-line',
+ 'panel-collapse': 'contract-left-line',
+ 'panel-expand': 'expand-right-line',
+ expand: 'expand-diagonal-line',
+ collapse: 'collapse-diagonal-line',
+ list: 'list-unordered',
+ sort: 'sort-desc',
+ grip: 'draggable',
+ }],
+ ['Files & tools', {
+ folder: 'folder-open-line',
+ 'folder-closed': 'folder-line',
+ 'folder-plus': 'folder-add-line',
+ 'folder-solid': 'folder-fill',
+ file: 'file-line',
+ 'file-text': 'file-text-line',
+ 'file-plus': 'file-add-line',
+ 'file-off': 'file-line',
+ 'image-off': 'image-line',
+ code: 'code-line',
+ terminal: 'terminal-box-line',
+ pencil: 'pencil-line',
+ glob: 'braces-line',
+ globe: 'global-line',
+ 'check-list': 'list-check',
+ bolt: 'flashlight-line',
+ 'git-pull-request': 'git-pull-request-line',
+ }],
+ ['Communication', {
+ message: 'message-line',
+ mail: 'mail-line',
+ user: 'user-line',
+ }],
+ ['Status & media', {
+ info: 'information-line',
+ 'help-circle': 'question-line',
+ 'alert-triangle': 'alert-line',
+ clock: 'time-line',
+ sparkles: 'sparkling-line',
+ play: 'play-fill',
+ stop: 'stop-fill',
+ star: 'star-fill',
+ 'star-outline': 'star-line',
+ 'dots-horizontal': 'more-line',
+ }],
+];
+
+const NAME_TO_REMIX = Object.assign({}, ...GROUPS.map(([, m]) => m));
+
+// --- resolve + validate ----------------------------------------------------
+const missing = [];
+const data = {};
+for (const [name, riName] of Object.entries(NAME_TO_REMIX)) {
+ const icon = getIconData(ri, riName);
+ if (!icon) {
+ missing.push(`${name} → ri:${riName}`);
+ continue;
+ }
+ data[name] = { body: icon.body, width: icon.width ?? 24, height: icon.height ?? 24 };
+}
+
+if (missing.length) {
+ console.error('gen-icon-data: the following Remix icons were not found in @iconify-json/ri:');
+ for (const m of missing) console.error(` ${m}`);
+ process.exit(1);
+}
+
+// --- emit ------------------------------------------------------------------
+const names = Object.keys(data);
+const keyOf = (n) => (/^[a-zA-Z_$][\w$]*$/.test(n) ? n : JSON.stringify(n));
+
+const lines = [];
+lines.push('// GENERATED FILE — do not edit by hand.');
+lines.push('// Source of truth: scripts/gen-icon-data.mjs (run `pnpm gen:icons`).');
+lines.push('// Icons are Remix Icon (ri) — https://remixicon.com/ — Apache-2.0.');
+lines.push('');
+lines.push('export type IconName =');
+for (let i = 0; i < names.length; i++) {
+ const suffix = i === names.length - 1 ? ';' : '';
+ lines.push(` | ${JSON.stringify(names[i])}${suffix}`);
+}
+lines.push('');
+lines.push('export interface IconData {');
+lines.push(' /** Inner SVG markup (paths/shapes), rendered inside our