@icen.ai/ui 0.4.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/AGENTS.md +256 -0
  2. package/README.md +47 -13
  3. package/dist/base.css +31 -4
  4. package/dist/behaviors/accordion.d.mts +4 -0
  5. package/dist/behaviors/accordion.mjs +4 -3
  6. package/dist/behaviors/ai-chat.d.mts +57 -0
  7. package/dist/behaviors/ai-chat.mjs +557 -0
  8. package/dist/behaviors/ai-composer.d.mts +112 -0
  9. package/dist/behaviors/ai-composer.mjs +2032 -0
  10. package/dist/behaviors/ai-core.d.mts +151 -0
  11. package/dist/behaviors/ai-core.mjs +491 -0
  12. package/dist/behaviors/ai-diff.d.mts +53 -0
  13. package/dist/behaviors/ai-diff.mjs +282 -0
  14. package/dist/behaviors/ai-panel.d.mts +98 -0
  15. package/dist/behaviors/ai-panel.mjs +1012 -0
  16. package/dist/behaviors/ai-provider.d.mts +198 -0
  17. package/dist/behaviors/ai-provider.mjs +1055 -0
  18. package/dist/behaviors/ai-tool.d.mts +18 -0
  19. package/dist/behaviors/ai-tool.mjs +2125 -0
  20. package/dist/behaviors/ai-tools.d.mts +81 -0
  21. package/dist/behaviors/ai-tools.mjs +1658 -0
  22. package/dist/behaviors/back-top.d.mts +20 -0
  23. package/dist/behaviors/back-top.mjs +101 -0
  24. package/dist/behaviors/carousel.d.mts +4 -0
  25. package/dist/behaviors/carousel.mjs +34 -16
  26. package/dist/behaviors/charts.d.mts +250 -0
  27. package/dist/behaviors/charts.mjs +1301 -448
  28. package/dist/behaviors/command-palette.d.mts +8 -0
  29. package/dist/behaviors/command-palette.mjs +189 -0
  30. package/dist/behaviors/context-menu.d.mts +36 -0
  31. package/dist/behaviors/context-menu.mjs +6 -2
  32. package/dist/behaviors/copy.d.mts +4 -0
  33. package/dist/behaviors/copy.mjs +7 -6
  34. package/dist/behaviors/datatable.d.mts +78 -0
  35. package/dist/behaviors/datatable.mjs +54 -11
  36. package/dist/behaviors/date-picker.d.mts +4 -0
  37. package/dist/behaviors/date-picker.mjs +296 -0
  38. package/dist/behaviors/dropdown.d.mts +4 -0
  39. package/dist/behaviors/dropdown.mjs +7 -3
  40. package/dist/behaviors/input.d.mts +4 -0
  41. package/dist/behaviors/input.mjs +184 -8
  42. package/dist/behaviors/modal.d.mts +11 -0
  43. package/dist/behaviors/modal.mjs +11 -6
  44. package/dist/behaviors/nav.d.mts +7 -0
  45. package/dist/behaviors/nav.mjs +25 -9
  46. package/dist/behaviors/notification.d.mts +172 -0
  47. package/dist/behaviors/notification.mjs +908 -0
  48. package/dist/behaviors/popover.d.mts +46 -0
  49. package/dist/behaviors/popover.mjs +2 -1
  50. package/dist/behaviors/select.d.mts +4 -0
  51. package/dist/behaviors/select.mjs +268 -34
  52. package/dist/behaviors/sidebar.d.mts +4 -0
  53. package/dist/behaviors/sidebar.mjs +4 -3
  54. package/dist/behaviors/slider.d.mts +4 -0
  55. package/dist/behaviors/slider.mjs +4 -3
  56. package/dist/behaviors/split-pane.d.mts +4 -0
  57. package/dist/behaviors/split-pane.mjs +102 -0
  58. package/dist/behaviors/tabs.d.mts +8 -0
  59. package/dist/behaviors/tabs.mjs +44 -19
  60. package/dist/behaviors/tag-input.d.mts +4 -0
  61. package/dist/behaviors/tag-input.mjs +12 -3
  62. package/dist/behaviors/theme.d.mts +25 -0
  63. package/dist/behaviors/theme.mjs +3 -1
  64. package/dist/behaviors/toast.d.mts +26 -0
  65. package/dist/behaviors/toast.mjs +24 -18
  66. package/dist/behaviors/tree.d.mts +4 -0
  67. package/dist/behaviors/tree.mjs +4 -3
  68. package/dist/behaviors/upload.d.mts +4 -0
  69. package/dist/behaviors/upload.mjs +205 -17
  70. package/dist/components/ai-chat.css +1037 -0
  71. package/dist/components/ai-chat.mjs +3 -0
  72. package/dist/components/ai-composer.mjs +3 -0
  73. package/dist/components/ai-context.mjs +2 -0
  74. package/dist/components/ai-diff.css +311 -0
  75. package/dist/components/ai-diff.mjs +3 -0
  76. package/dist/components/ai-files.mjs +3 -0
  77. package/dist/components/ai-message.mjs +3 -0
  78. package/dist/components/ai-panel.css +682 -0
  79. package/dist/components/ai-reasoning.mjs +3 -0
  80. package/dist/components/ai-subagent.mjs +2 -0
  81. package/dist/components/ai-todo.mjs +2 -0
  82. package/dist/components/ai-tool-call.mjs +2 -0
  83. package/dist/components/ai-tool.css +532 -0
  84. package/dist/components/ai-usage.mjs +3 -0
  85. package/dist/components/back-top.css +88 -0
  86. package/dist/components/back-top.mjs +3 -0
  87. package/dist/components/badge.css +127 -0
  88. package/dist/components/badge.mjs +2 -0
  89. package/dist/components/breadcrumb.css +53 -0
  90. package/dist/components/btn.css +4 -7
  91. package/dist/components/card.css +66 -1
  92. package/dist/components/chart-bar.css +10 -0
  93. package/dist/components/chart-calendar.mjs +5 -0
  94. package/dist/components/chart-gauge.css +1 -1
  95. package/dist/components/chart-heatmap.css +40 -0
  96. package/dist/components/chart-scatter.css +24 -0
  97. package/dist/components/chart-scatter.mjs +4 -0
  98. package/dist/components/charts.css +86 -1
  99. package/dist/components/charts.mjs +1 -0
  100. package/dist/components/command-palette.css +246 -0
  101. package/dist/components/command-palette.mjs +3 -0
  102. package/dist/components/content.css +221 -8
  103. package/dist/components/copy.css +93 -0
  104. package/dist/components/copy.mjs +3 -0
  105. package/dist/components/datatable.css +4 -8
  106. package/dist/components/date-picker.css +240 -0
  107. package/dist/components/date-picker.mjs +3 -0
  108. package/dist/components/empty.css +69 -3
  109. package/dist/components/feedback.css +241 -10
  110. package/dist/components/form.css +129 -11
  111. package/dist/components/form.mjs +2 -0
  112. package/dist/components/input.css +25 -2
  113. package/dist/components/layout.css +36 -36
  114. package/dist/components/media.css +153 -6
  115. package/dist/components/menu.css +2 -2
  116. package/dist/components/modal.css +1 -1
  117. package/dist/components/nav.css +106 -2
  118. package/dist/components/notification.css +387 -0
  119. package/dist/components/notification.mjs +3 -0
  120. package/dist/components/pagination.css +1 -1
  121. package/dist/components/panel.css +104 -9
  122. package/dist/components/pill.css +82 -12
  123. package/dist/components/scroll-area.css +112 -0
  124. package/dist/components/scroll-area.mjs +2 -0
  125. package/dist/components/segmented.css +80 -5
  126. package/dist/components/segmented.mjs +1 -0
  127. package/dist/components/select.css +104 -17
  128. package/dist/components/sidebar.css +119 -1
  129. package/dist/components/slider.css +113 -0
  130. package/dist/components/split-pane.css +111 -0
  131. package/dist/components/split-pane.mjs +3 -0
  132. package/dist/components/stat.css +131 -12
  133. package/dist/components/steps.css +98 -1
  134. package/dist/components/switch.css +145 -7
  135. package/dist/components/table.css +111 -29
  136. package/dist/components/tabs.css +132 -22
  137. package/dist/components/tag-input.css +4 -4
  138. package/dist/components/tag.css +164 -0
  139. package/dist/components/tag.mjs +2 -0
  140. package/dist/components/toast.css +122 -5
  141. package/dist/components/toolbar.css +101 -0
  142. package/dist/components/toolbar.mjs +2 -0
  143. package/dist/components/tooltip.css +2 -2
  144. package/dist/components/upload.css +61 -4
  145. package/dist/index.d.mts +37 -0
  146. package/dist/index.mjs +9409 -929
  147. package/dist/registry.json +176 -4
  148. package/dist/retro-effects.css +21 -17
  149. package/dist/tokens/colors.css +4 -1
  150. package/dist/tokens/retro-effects.css +21 -17
  151. package/dist/tokens/style-profiles.css +21 -0
  152. package/dist/tokens.css +25 -1
  153. package/dist/ui.css +9616 -2839
  154. package/docs/spec/ai-native.md +659 -0
  155. package/package.json +15 -5
@@ -0,0 +1,557 @@
1
+ // src/behaviors/ai-core.ts
2
+ var svg = (body) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
3
+ var ICON_TERMINAL = svg('<path d="m4 17 6-6-6-6"/><path d="M12 19h8"/>');
4
+ var ICON_FILE = svg(
5
+ '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>'
6
+ );
7
+ var ICON_FILE_PEN = svg(
8
+ '<path d="M12.5 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8l5 5v3.5"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M19 6.5 16.5 4"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/>'
9
+ );
10
+ var ICON_FILE_PLUS = svg(
11
+ '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M9 15h6"/><path d="M12 12v6"/>'
12
+ );
13
+ var ICON_TRASH = svg(
14
+ '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/>'
15
+ );
16
+ var ICON_SEARCH = svg('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>');
17
+ var ICON_FOLDER_SEARCH = svg(
18
+ '<path d="M11 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v4"/><circle cx="17" cy="17" r="3"/><path d="m21 21-1.9-1.9"/>'
19
+ );
20
+ var ICON_SCAN_SEARCH = svg(
21
+ '<path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="3"/><path d="m16 16-1.9-1.9"/>'
22
+ );
23
+ var ICON_GLOBE = svg(
24
+ '<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>'
25
+ );
26
+ var ICON_DOWNLOAD = svg(
27
+ '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>'
28
+ );
29
+ var ICON_BLOCKS = svg(
30
+ '<rect width="7" height="7" x="4" y="4" rx="1"/><rect width="7" height="7" x="13" y="4" rx="1"/><rect width="7" height="7" x="4" y="13" rx="1"/><rect width="7" height="7" x="13" y="13" rx="1"/>'
31
+ );
32
+ var ICON_ZAP = svg(
33
+ '<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>'
34
+ );
35
+ var ICON_LIST = svg(
36
+ '<path d="M3 12h.01"/><path d="M3 18h.01"/><path d="M3 6h.01"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M8 6h13"/>'
37
+ );
38
+ var ICON_LIST_CHECKS = svg(
39
+ '<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/>'
40
+ );
41
+ var ICON_BOT = svg(
42
+ '<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>'
43
+ );
44
+ var ICON_CHART = svg(
45
+ '<path d="M3 3v18h18"/><path d="M7 15l4-6 3 4 5-8"/>'
46
+ );
47
+ var ICON_FILE_TEXT = svg(
48
+ '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>'
49
+ );
50
+ function isObj(v) {
51
+ return typeof v === "object" && v !== null;
52
+ }
53
+ function pickStr(input, keys) {
54
+ if (!isObj(input)) return void 0;
55
+ for (const k of keys) {
56
+ const v = input[k];
57
+ if (typeof v === "string" && v) return v;
58
+ }
59
+ return void 0;
60
+ }
61
+ function pickNum(src, keys) {
62
+ if (!isObj(src)) return void 0;
63
+ for (const k of keys) {
64
+ const v = src[k];
65
+ if (typeof v === "number" && Number.isFinite(v)) return v;
66
+ }
67
+ return void 0;
68
+ }
69
+ function firstLine(s, max = 80) {
70
+ const line = s.split("\n", 1)[0]?.trim() ?? "";
71
+ return line.length > max ? `${line.slice(0, max)}\u2026` : line;
72
+ }
73
+ function filePathOf(input) {
74
+ return pickStr(input, ["file_path", "path", "file", "filename"]);
75
+ }
76
+ var summarizePath = (input) => filePathOf(input);
77
+ function summarizeRead(input) {
78
+ const p = filePathOf(input);
79
+ if (!p) return void 0;
80
+ const offset = pickNum(input, ["offset", "start_line"]);
81
+ if (offset == null) return p;
82
+ const limit = pickNum(input, ["limit", "end_line"]);
83
+ return limit != null ? `${p}:${offset}-${offset + limit}` : `${p}:${offset}`;
84
+ }
85
+ function summarizeShell(input) {
86
+ const cmd = typeof input === "string" ? input : pickStr(input, ["command", "cmd", "script"]);
87
+ return cmd ? firstLine(cmd) : void 0;
88
+ }
89
+ function summarizeGrep(input) {
90
+ const pattern = pickStr(input, ["pattern", "regex", "query"]);
91
+ const p = filePathOf(input);
92
+ if (pattern) return p ? `${firstLine(pattern, 40)} \xB7 ${p}` : firstLine(pattern);
93
+ return p;
94
+ }
95
+ var sum = (fn) => {
96
+ return (input) => fn(input) ?? "";
97
+ };
98
+ var BUILTIN_KINDS = {
99
+ shell: { label: "\u547D\u4EE4", icon: ICON_TERMINAL, tint: "accent", summarize: sum(summarizeShell) },
100
+ read: { label: "\u8BFB\u53D6", icon: ICON_FILE, tint: "info", summarize: sum(summarizeRead) },
101
+ edit: { label: "\u7F16\u8F91", icon: ICON_FILE_PEN, tint: "warning", summarize: sum(summarizePath) },
102
+ write: { label: "\u5199\u5165", icon: ICON_FILE_PLUS, tint: "warning", summarize: sum(summarizePath) },
103
+ rm: { label: "\u5220\u9664", icon: ICON_TRASH, tint: "error", summarize: sum(summarizePath) },
104
+ grep: { label: "\u641C\u7D22", icon: ICON_SEARCH, tint: "info", summarize: sum(summarizeGrep) },
105
+ glob: { label: "\u5339\u914D", icon: ICON_FOLDER_SEARCH, tint: "info", summarize: sum((i) => pickStr(i, ["pattern", "glob", "path"])) },
106
+ browser: { label: "\u6D4F\u89C8", icon: ICON_GLOBE, tint: "accent", summarize: sum((i) => pickStr(i, ["url", "uri", "target"])) },
107
+ search: { label: "\u68C0\u7D22", icon: ICON_SCAN_SEARCH, tint: "accent", summarize: sum((i) => pickStr(i, ["query", "q", "pattern"])) },
108
+ fetch: { label: "\u8BF7\u6C42", icon: ICON_DOWNLOAD, tint: "info", summarize: sum((i) => pickStr(i, ["url", "uri", "endpoint"])) },
109
+ mcp: { label: "MCP", icon: ICON_BLOCKS, tint: "muted", summarize: sum((i) => pickStr(i, ["tool", "method", "name"])) },
110
+ skill: { label: "\u6280\u80FD", icon: ICON_ZAP, tint: "accent", summarize: sum((i) => pickStr(i, ["skill", "name", "command"])) },
111
+ todo: {
112
+ label: "\u5F85\u529E",
113
+ icon: ICON_LIST,
114
+ tint: "info",
115
+ summarize: sum((input) => {
116
+ const items = input && typeof input === "object" && Array.isArray(input.items) ? input.items : Array.isArray(input) ? input : [];
117
+ const done = items.filter((it) => it?.status === "done").length;
118
+ return items.length > 0 ? `${done}/${items.length} \u5B8C\u6210` : "";
119
+ })
120
+ },
121
+ plan: { label: "\u8BA1\u5212", icon: ICON_LIST_CHECKS, tint: "info" },
122
+ subagent: { label: "\u5B50\u4EE3\u7406", icon: ICON_BOT, tint: "accent", summarize: sum((i) => pickStr(i, ["task", "description", "prompt", "message", "subagent_type"])) },
123
+ note: { label: "\u7B14\u8BB0", icon: ICON_FILE_TEXT, tint: "muted" },
124
+ chart: { label: "\u56FE\u8868", icon: ICON_CHART, tint: "info", summarize: sum((i) => pickStr(i, ["title", "name"])) }
125
+ };
126
+ var kindRegistry = new Map(Object.entries(BUILTIN_KINDS));
127
+ function trim1(v) {
128
+ const s = (Math.round(v * 10) / 10).toFixed(1);
129
+ return s.endsWith(".0") ? s.slice(0, -2) : s;
130
+ }
131
+ function formatDuration(ms) {
132
+ if (!Number.isFinite(ms) || ms <= 0) return "0ms";
133
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
134
+ if (ms < 6e4) return `${trim1(ms / 1e3)}s`;
135
+ const m = Math.floor(ms / 6e4);
136
+ const s = Math.round(ms % 6e4 / 1e3);
137
+ if (ms < 36e5) return s > 0 ? `${m}m${s}s` : `${m}m`;
138
+ const h = Math.floor(ms / 36e5);
139
+ const rm = Math.round(ms % 36e5 / 6e4);
140
+ return rm > 0 ? `${h}h${rm}m` : `${h}h`;
141
+ }
142
+ function aiContentUrl(part) {
143
+ if (part.type === "text" || part.type === "resource-link") return void 0;
144
+ if (typeof part.url === "string" && part.url) return part.url;
145
+ if (typeof part.data === "string" && part.data && typeof part.mimeType === "string") {
146
+ return `data:${part.mimeType};base64,${part.data}`;
147
+ }
148
+ return void 0;
149
+ }
150
+ function svgIcon(svg2) {
151
+ if (typeof document === "undefined" || typeof DOMParser === "undefined") return null;
152
+ let parsed;
153
+ try {
154
+ parsed = new DOMParser().parseFromString(svg2, "image/svg+xml");
155
+ } catch {
156
+ return null;
157
+ }
158
+ const root = parsed.documentElement;
159
+ if (!root || root.tagName.toLowerCase() !== "svg" || parsed.querySelector("parsererror")) {
160
+ return null;
161
+ }
162
+ const sanitize = (el2) => {
163
+ for (const attr of Array.from(el2.attributes)) {
164
+ const name = attr.name.toLowerCase();
165
+ if (name.startsWith("on")) el2.removeAttribute(attr.name);
166
+ if ((name === "href" || name === "xlink:href") && attr.value.trim().toLowerCase().startsWith("javascript:")) {
167
+ el2.removeAttribute(attr.name);
168
+ }
169
+ }
170
+ for (const child of Array.from(el2.children)) {
171
+ const tag = child.tagName.toLowerCase();
172
+ if (tag === "script" || tag === "foreignobject") {
173
+ child.remove();
174
+ continue;
175
+ }
176
+ sanitize(child);
177
+ }
178
+ };
179
+ sanitize(root);
180
+ return document.importNode(root, true);
181
+ }
182
+
183
+ // src/behaviors/ai-chat.ts
184
+ var ICON_COPY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="2" width="8" height="4" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>';
185
+ var ICON_RETRY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>';
186
+ function isBrowser() {
187
+ return typeof document !== "undefined" && typeof window !== "undefined";
188
+ }
189
+ function prefersReducedMotion() {
190
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
191
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
192
+ }
193
+ function emit(target, name, detail) {
194
+ target.dispatchEvent(new CustomEvent(name, { detail, bubbles: true }));
195
+ }
196
+ function fallbackCopy(text) {
197
+ const ta = document.createElement("textarea");
198
+ ta.value = text;
199
+ ta.style.position = "fixed";
200
+ ta.style.opacity = "0";
201
+ document.body.appendChild(ta);
202
+ ta.select();
203
+ try {
204
+ document.execCommand("copy");
205
+ } catch {
206
+ }
207
+ ta.remove();
208
+ }
209
+ function copyText(text) {
210
+ if (typeof navigator !== "undefined" && navigator.clipboard) {
211
+ navigator.clipboard.writeText(text).catch(() => fallbackCopy(text));
212
+ } else {
213
+ fallbackCopy(text);
214
+ }
215
+ }
216
+ function setupChat(chat) {
217
+ const el2 = chat;
218
+ if (el2.__icenAiChatInit) return void 0;
219
+ el2.__icenAiChatInit = true;
220
+ const scrollEl = chat.querySelector(".ai-chat-scroll");
221
+ let jump = chat.querySelector(".ai-chat-jump") ?? null;
222
+ let pinned = true;
223
+ let unread = 0;
224
+ let lastMsgCount = 0;
225
+ let suppressUntil = 0;
226
+ let rafPending = false;
227
+ const NEAR_BOTTOM_PX = 32;
228
+ const isAtBottom = () => scrollEl ? scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < NEAR_BOTTOM_PX : true;
229
+ const updateJump = () => {
230
+ if (!jump) return;
231
+ if (pinned) {
232
+ jump.hidden = true;
233
+ return;
234
+ }
235
+ jump.textContent = unread > 0 ? `\u56DE\u5230\u5E95\u90E8 \xB7 ${unread} \u6761\u65B0\u6D88\u606F` : "\u56DE\u5230\u5E95\u90E8";
236
+ jump.hidden = false;
237
+ };
238
+ const scrollToBottom = (smooth) => {
239
+ if (!scrollEl) return;
240
+ const behavior = smooth && !prefersReducedMotion() ? "smooth" : "auto";
241
+ suppressUntil = Date.now() + (behavior === "smooth" ? 600 : 100);
242
+ scrollEl.scrollTo({ top: scrollEl.scrollHeight, behavior });
243
+ };
244
+ const onContentChange = () => {
245
+ rafPending = false;
246
+ if (!scrollEl) return;
247
+ if (pinned) {
248
+ scrollToBottom(false);
249
+ } else {
250
+ const count = scrollEl.querySelectorAll(".ai-msg").length;
251
+ if (count >= lastMsgCount) {
252
+ unread += count - lastMsgCount;
253
+ } else {
254
+ unread = 0;
255
+ }
256
+ lastMsgCount = Math.max(lastMsgCount, count);
257
+ updateJump();
258
+ }
259
+ };
260
+ const scheduleContentCheck = () => {
261
+ if (rafPending) return;
262
+ rafPending = true;
263
+ window.requestAnimationFrame(onContentChange);
264
+ };
265
+ const onScroll = () => {
266
+ if (Date.now() < suppressUntil) return;
267
+ const atBottom = isAtBottom();
268
+ if (atBottom && !pinned) {
269
+ pinned = true;
270
+ unread = 0;
271
+ updateJump();
272
+ } else if (!atBottom && pinned) {
273
+ pinned = false;
274
+ updateJump();
275
+ }
276
+ };
277
+ const onJumpClick = () => {
278
+ pinned = true;
279
+ unread = 0;
280
+ updateJump();
281
+ scrollToBottom(true);
282
+ };
283
+ const onClick = (e) => {
284
+ const target = e.target;
285
+ if (!(target instanceof Element)) return;
286
+ const actionBtn = target.closest("[data-ai-msg-action]");
287
+ if (actionBtn) {
288
+ const msg = actionBtn.closest(".ai-msg");
289
+ if (!msg) return;
290
+ const action = actionBtn.dataset.aiMsgAction;
291
+ if (action === "copy") {
292
+ copyText(msg.querySelector(".ai-msg-body")?.textContent ?? "");
293
+ emit(msg, "icen:ai-copy", { el: msg });
294
+ } else if (action === "retry") {
295
+ emit(msg, "icen:ai-retry", { el: msg });
296
+ }
297
+ return;
298
+ }
299
+ const head = target.closest(".ai-reasoning-head");
300
+ if (head) {
301
+ const reasoning = head.closest(".ai-reasoning");
302
+ if (!reasoning) return;
303
+ const body = reasoning.querySelector(".ai-reasoning-body");
304
+ const open = head.getAttribute("aria-expanded") !== "true";
305
+ head.setAttribute("aria-expanded", String(open));
306
+ reasoning.classList.toggle("is-open", open);
307
+ if (body) body.hidden = !open;
308
+ emit(reasoning, "icen:ai-toggle", { el: reasoning, open });
309
+ }
310
+ };
311
+ const disposers = [];
312
+ let resizeObs;
313
+ let mutationObs;
314
+ if (scrollEl) {
315
+ if (!jump) {
316
+ jump = document.createElement("button");
317
+ jump.type = "button";
318
+ jump.className = "ai-chat-jump";
319
+ jump.hidden = true;
320
+ chat.appendChild(jump);
321
+ }
322
+ lastMsgCount = scrollEl.querySelectorAll(".ai-msg").length;
323
+ scrollEl.addEventListener("scroll", onScroll, { passive: true });
324
+ jump.addEventListener("click", onJumpClick);
325
+ const jumpBtn = jump;
326
+ disposers.push(() => {
327
+ scrollEl.removeEventListener("scroll", onScroll);
328
+ jumpBtn.removeEventListener("click", onJumpClick);
329
+ });
330
+ if (typeof ResizeObserver !== "undefined") {
331
+ resizeObs = new ResizeObserver(scheduleContentCheck);
332
+ resizeObs.observe(scrollEl);
333
+ for (const child of Array.from(scrollEl.children)) resizeObs.observe(child);
334
+ disposers.push(() => resizeObs?.disconnect());
335
+ }
336
+ if (typeof MutationObserver !== "undefined") {
337
+ mutationObs = new MutationObserver((records) => {
338
+ if (resizeObs) {
339
+ for (const rec of records) {
340
+ for (const node of Array.from(rec.addedNodes)) {
341
+ if (node instanceof Element) resizeObs.observe(node);
342
+ }
343
+ }
344
+ }
345
+ scheduleContentCheck();
346
+ });
347
+ mutationObs.observe(scrollEl, { childList: true, subtree: true, characterData: true });
348
+ disposers.push(() => mutationObs?.disconnect());
349
+ }
350
+ scrollToBottom(false);
351
+ }
352
+ chat.addEventListener("click", onClick);
353
+ disposers.push(() => chat.removeEventListener("click", onClick));
354
+ return () => {
355
+ for (const dispose of disposers) dispose();
356
+ el2.__icenAiChatInit = false;
357
+ };
358
+ }
359
+ function initAiChat(root) {
360
+ const cleanups = [];
361
+ if (isBrowser()) {
362
+ const scope = root ?? document;
363
+ const candidates = [];
364
+ if (scope instanceof Element && scope.matches(".ai-chat")) {
365
+ candidates.push(scope);
366
+ }
367
+ candidates.push(...Array.from(scope.querySelectorAll(".ai-chat")));
368
+ for (const chat of candidates) {
369
+ const cleanup = setupChat(chat);
370
+ if (cleanup) cleanups.push(cleanup);
371
+ }
372
+ }
373
+ return () => {
374
+ for (const fn of cleanups) fn();
375
+ cleanups.length = 0;
376
+ };
377
+ }
378
+ function createAiStream(el2) {
379
+ const reasoning = el2.closest(".ai-reasoning");
380
+ const startedAt = Date.now();
381
+ let state = "streaming";
382
+ el2.classList.add("is-streaming");
383
+ el2.setAttribute("aria-busy", "true");
384
+ reasoning?.classList.add("is-streaming");
385
+ const settle = (finished) => {
386
+ if (state !== "streaming") return;
387
+ state = finished ? "done" : "cancelled";
388
+ el2.classList.remove("is-streaming");
389
+ el2.removeAttribute("aria-busy");
390
+ if (!reasoning) return;
391
+ reasoning.classList.remove("is-streaming");
392
+ if (!finished) return;
393
+ const head = reasoning.querySelector(".ai-reasoning-head");
394
+ const body = reasoning.querySelector(".ai-reasoning-body");
395
+ head?.setAttribute("aria-expanded", "false");
396
+ if (body) body.hidden = true;
397
+ const time = reasoning.querySelector(".ai-reasoning-time");
398
+ if (time) time.textContent = formatDuration(Date.now() - startedAt);
399
+ };
400
+ return {
401
+ append(text) {
402
+ if (state !== "streaming" || !text) return;
403
+ el2.textContent += text;
404
+ },
405
+ done() {
406
+ settle(true);
407
+ },
408
+ cancel() {
409
+ settle(false);
410
+ },
411
+ fail() {
412
+ if (state !== "streaming") return;
413
+ state = "error";
414
+ el2.classList.remove("is-streaming");
415
+ el2.removeAttribute("aria-busy");
416
+ el2.classList.add("is-error");
417
+ reasoning?.classList.remove("is-streaming");
418
+ }
419
+ };
420
+ }
421
+ var ROLE_AVATAR = {
422
+ user: "\u6211",
423
+ assistant: "AI",
424
+ system: "\u7CFB",
425
+ tool: "\u5DE5"
426
+ };
427
+ function safeHref(uri) {
428
+ const s = uri.trim().toLowerCase();
429
+ if (s.startsWith("javascript:") || s.startsWith("data:text/html")) return void 0;
430
+ return uri;
431
+ }
432
+ function el(tag, className, text) {
433
+ const node = document.createElement(tag);
434
+ if (className) node.className = className;
435
+ if (text != null) node.textContent = text;
436
+ return node;
437
+ }
438
+ function renderAiContentPart(part) {
439
+ if (part.type === "text") {
440
+ const span = el("span", "ai-msg-text");
441
+ span.textContent = part.text;
442
+ if (part.state === "streaming") span.classList.add("is-streaming");
443
+ return span;
444
+ }
445
+ if (part.type === "image") {
446
+ const src = aiContentUrl(part);
447
+ if (!src) return void 0;
448
+ const img = document.createElement("img");
449
+ img.className = "ai-msg-media ai-msg-media--image";
450
+ img.alt = part.alt ?? "";
451
+ img.setAttribute("loading", "lazy");
452
+ img.setAttribute("src", src);
453
+ return img;
454
+ }
455
+ if (part.type === "audio") {
456
+ const src = aiContentUrl(part);
457
+ if (!src) return void 0;
458
+ const audio = document.createElement("audio");
459
+ audio.className = "ai-msg-media ai-msg-media--audio";
460
+ audio.controls = true;
461
+ audio.setAttribute("src", src);
462
+ return audio;
463
+ }
464
+ if (part.type === "video") {
465
+ const src = aiContentUrl(part);
466
+ if (!src) return void 0;
467
+ const video = document.createElement("video");
468
+ video.className = "ai-msg-media ai-msg-media--video";
469
+ video.controls = true;
470
+ video.setAttribute("playsinline", "");
471
+ video.setAttribute("src", src);
472
+ return video;
473
+ }
474
+ if (part.type === "file") {
475
+ const chip = el("span", "ai-msg-file");
476
+ chip.appendChild(el("span", "ai-msg-file-name", part.filename ?? "\u6587\u4EF6"));
477
+ const href2 = part.url ? safeHref(part.url) : void 0;
478
+ if (href2 && /^(https?:|data:)/i.test(href2)) {
479
+ const a2 = document.createElement("a");
480
+ a2.className = "ai-msg-file-link";
481
+ a2.href = href2;
482
+ a2.download = part.filename ?? "";
483
+ a2.textContent = "\u4E0B\u8F7D";
484
+ chip.appendChild(a2);
485
+ }
486
+ return chip;
487
+ }
488
+ const a = document.createElement("a");
489
+ a.className = "ai-msg-link";
490
+ const href = safeHref(part.uri);
491
+ if (href) a.href = href;
492
+ a.textContent = part.name ?? part.uri;
493
+ a.title = part.uri;
494
+ return a;
495
+ }
496
+ function renderAiMessage(scrollEl, model) {
497
+ const row = el("div", `ai-msg ai-msg--${model.role}`);
498
+ const avatar = el("span", "ai-msg-avatar", ROLE_AVATAR[model.role] ?? "");
499
+ const body = el("div", "ai-msg-body");
500
+ row.appendChild(avatar);
501
+ row.appendChild(body);
502
+ const parts = typeof model.content === "string" ? [{ type: "text", text: model.content }] : model.content;
503
+ for (const part of parts) {
504
+ const node = renderAiContentPart(part);
505
+ if (node) body.appendChild(node);
506
+ }
507
+ if (model.model) body.appendChild(el("span", "ai-msg-model", model.model));
508
+ const metaEl = el("div", "ai-msg-meta");
509
+ if (model.meta != null) metaEl.textContent = model.meta;
510
+ row.appendChild(metaEl);
511
+ const actions = el("div", "ai-msg-actions");
512
+ const copyBtn = el("button", "ai-msg-action");
513
+ copyBtn.type = "button";
514
+ copyBtn.setAttribute("data-ai-msg-action", "copy");
515
+ copyBtn.setAttribute("aria-label", "\u590D\u5236");
516
+ const copyIcon = svgIcon(ICON_COPY);
517
+ if (copyIcon) copyBtn.appendChild(copyIcon);
518
+ const retryBtn = el("button", "ai-msg-action");
519
+ retryBtn.type = "button";
520
+ retryBtn.setAttribute("data-ai-msg-action", "retry");
521
+ retryBtn.setAttribute("aria-label", "\u91CD\u8BD5");
522
+ const retryIcon = svgIcon(ICON_RETRY);
523
+ if (retryIcon) retryBtn.appendChild(retryIcon);
524
+ actions.append(copyBtn, retryBtn);
525
+ row.appendChild(actions);
526
+ const handle = {
527
+ el: row,
528
+ body,
529
+ setMeta(meta) {
530
+ metaEl.textContent = meta;
531
+ },
532
+ setError(message) {
533
+ row.classList.add("ai-msg--error");
534
+ let errEl = row.querySelector(".ai-msg-error-text");
535
+ if (!errEl) {
536
+ errEl = el("div", "ai-msg-error-text");
537
+ body.appendChild(errEl);
538
+ }
539
+ errEl.textContent = message;
540
+ },
541
+ stream() {
542
+ const span = el("span", "ai-msg-stream-text");
543
+ body.appendChild(span);
544
+ return createAiStream(span);
545
+ }
546
+ };
547
+ if (model.error != null) handle.setError(model.error);
548
+ if (model.streaming) row.classList.add("is-streaming");
549
+ scrollEl.appendChild(row);
550
+ return handle;
551
+ }
552
+ export {
553
+ createAiStream,
554
+ initAiChat,
555
+ renderAiContentPart,
556
+ renderAiMessage
557
+ };
@@ -0,0 +1,112 @@
1
+ import { AiUsage } from './ai-core.js';
2
+ import { AiTodoItem, AiUsageRingOpts } from './ai-panel.js';
3
+ import { AiClient, AiAuditor } from './ai-provider.js';
4
+
5
+ interface AiProviderModelOption {
6
+ id: string;
7
+ label: string;
8
+ /** 上下文窗口 token 数(如 1_000_000 → 弹层显示 1M) */
9
+ context?: number;
10
+ }
11
+ interface AiProviderOption {
12
+ id: string;
13
+ label: string;
14
+ /** 单色 SVG 字符串(经 svgIcon 消毒);缺失时用首字母圆形章(token 色) */
15
+ icon?: string;
16
+ models: AiProviderModelOption[];
17
+ }
18
+ interface AiComposerCurrentModel {
19
+ provider: string;
20
+ model: string;
21
+ }
22
+ interface AiComposerCommand {
23
+ name: string;
24
+ description?: string;
25
+ /** 参数提示(如 "<path>"),弹层命令行右侧灰色展示 */
26
+ argsHint?: string;
27
+ }
28
+ type AiComposerRefKind = 'file' | 'folder' | 'doc' | 'agent';
29
+ interface AiComposerRefSource {
30
+ kind: AiComposerRefKind;
31
+ id: string;
32
+ label: string;
33
+ sub?: string;
34
+ }
35
+ /**
36
+ * 运行态切换(spec §4.3):
37
+ * el 传 composer 容器或其内部任意元素(含发送钮自身),就近解析 [data-ai-composer]。
38
+ */
39
+ declare function setComposerRunning(el: HTMLElement, running: boolean): void;
40
+ /**
41
+ * 初始化 root 下的全部 [data-ai-composer](root 自身匹配时包含自身)。
42
+ * 幂等;返回销毁函数(复刻 initBackTop 约定)。
43
+ */
44
+ declare function initAiComposer(root?: ParentNode): () => void;
45
+ /**
46
+ * 配置模型切换(§8):工具条出现模型钮,弹层按 provider 分组 + 搜索过滤 + 当前勾选。
47
+ * current 缺省取第一个 provider 的第一个模型(静默选中,不派事件)。
48
+ * 选择派 icen:ai-model-change {provider, model, label, context};
49
+ * 选中模型的 context 自动成为 setComposerUsage 的 total 默认值(显式 opts.total 优先)。
50
+ * providers 传空数组 = 清除配置(移除 JS 自动创建的模型钮)。
51
+ */
52
+ declare function setComposerModels(el: HTMLElement, providers: AiProviderOption[], current?: AiComposerCurrentModel): void;
53
+ /**
54
+ * 配置斜杠命令(§8):输入开头 `/` 触发命令弹层(随输入过滤,↑↓/Enter/Tab/Esc)。
55
+ * 选中派 icen:ai-command {name, args} 并清空输入(命令被拦截执行、不进消息流)。
56
+ * commands 传空数组 = 清除配置。
57
+ */
58
+ declare function setComposerCommands(el: HTMLElement, commands: AiComposerCommand[]): void;
59
+ /**
60
+ * 配置 @ 引用源(§8):任意位置 token 起点 `@` 触发引用弹层(按 kind 分组:file/folder/doc/agent)。
61
+ * 选中后文本插入 `@label ` + refs chip +1,派 icen:ai-ref {action:'add', ref};
62
+ * chip × 移除 chip 并删文本里首个 `@label`,派 {action:'remove', ref}。
63
+ */
64
+ declare function setComposerRefs(el: HTMLElement, sources: AiComposerRefSource[]): void;
65
+ /**
66
+ * 配置用量环(§8):工具条右侧挂 renderAiUsageRing(点击环弹出完整分段分解)。
67
+ * total 缺省时用选中模型的 context(setComposerModels 闭环);显式 opts.total 优先。
68
+ * usage 传 null = 清除配置(移除 JS 自动创建的挂载点)。
69
+ */
70
+ declare function setComposerUsage(el: HTMLElement, usage: AiUsage | null, opts?: AiUsageRingOpts): void;
71
+ /**
72
+ * 输入框旁的实时待办(业界模式:plan/todo 是工具调用,实时状态挂在输入区而非对话流):
73
+ * chip = 图标 + x/y,状态色随进度(进行中 accent / 全部完成 success / 有失败 warning);
74
+ * 点击弹出完整清单(popover + renderAiTodo,只读)。传 null 清除。
75
+ */
76
+ declare function setComposerTodo(el: HTMLElement, items: AiTodoItem[] | null): void;
77
+ interface AiComposerBindOpts {
78
+ /**
79
+ * 传入即托管对话(client 模式):icen:ai-send → renderAiMessage 用户消息 + running +
80
+ * client.stream → assistant 消息 + createAiStream 逐 chunk 追加 → done 收尾(meta 自动填
81
+ * token/成本);错误 → setError + fail;stop → session.cancel();排队消息每轮完成自动发送。
82
+ * 需要 messages 容器。history 由绑定层维护(system 消息可先经 client 直接对话时自带)。
83
+ */
84
+ client?: AiClient;
85
+ /** 消息挂载容器(.ai-chat-scroll 或任意元素;client 模式必填) */
86
+ messages?: HTMLElement;
87
+ /**
88
+ * 用量环自动更新(监听 document 的 icen:ai-done):
89
+ * 'context'(默认)= 上下文估算口径(ai-core contextEstimate——环语义正确);
90
+ * 'billing' = 请求用量累加(计费视角);传 AiAuditor 实例 = auditor.summary()。
91
+ * total 缺省沿用 setComposerModels 的模型 context 闭环。
92
+ */
93
+ usage?: {
94
+ from: 'context' | 'billing' | AiAuditor;
95
+ total?: number;
96
+ };
97
+ /** 运行态自动管理(默认 true):icen:ai-send → running;icen:ai-done → 解除 */
98
+ running?: boolean;
99
+ }
100
+ interface AiComposerBinding {
101
+ /** 解绑全部监听并中止进行中的托管会话(composer 本身的 init 不受影响) */
102
+ unbind(): void;
103
+ }
104
+ /**
105
+ * composer ↔ 消息区 ↔ client ↔ 用量环的闭环绑定(事件驱动,传输会话所有权仍归消费方)。
106
+ * 不传 client 时为纯状态绑定(send→running、done→解除、环更新),渐进采用。
107
+ * 注意:running 自动管理依赖 client 派发的 icen:ai-done;无 client 的纯事件用法需
108
+ * 自行派发该事件或传 running:false。
109
+ */
110
+ declare function bindComposer(el: HTMLElement, opts?: AiComposerBindOpts): AiComposerBinding;
111
+
112
+ export { type AiComposerBindOpts, type AiComposerBinding, type AiComposerCommand, type AiComposerCurrentModel, type AiComposerRefKind, type AiComposerRefSource, type AiProviderModelOption, type AiProviderOption, bindComposer, initAiComposer, setComposerCommands, setComposerModels, setComposerRefs, setComposerRunning, setComposerTodo, setComposerUsage };