work-agent 0.1.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 (245) hide show
  1. package/README.md +234 -0
  2. package/app/(admin)/approvals/page.tsx +16 -0
  3. package/app/(admin)/audit/page.tsx +18 -0
  4. package/app/(admin)/layout.tsx +47 -0
  5. package/app/(admin)/scheduled-tasks/page.tsx +17 -0
  6. package/app/(admin)/settings/page.tsx +46 -0
  7. package/app/(admin)/skills/[name]/page.tsx +378 -0
  8. package/app/(admin)/skills/page.tsx +406 -0
  9. package/app/(admin)/statistics/page.tsx +416 -0
  10. package/app/(admin)/tickets/[id]/page.tsx +348 -0
  11. package/app/(admin)/tickets/new/page.tsx +309 -0
  12. package/app/(admin)/tickets/page.tsx +27 -0
  13. package/app/api/audit/route.ts +30 -0
  14. package/app/api/auth/feishu/callback/route.ts +72 -0
  15. package/app/api/auth/feishu/login/route.ts +17 -0
  16. package/app/api/auth/feishu/sso/route.ts +78 -0
  17. package/app/api/auth/login/route.ts +85 -0
  18. package/app/api/auth/oauth/route.ts +168 -0
  19. package/app/api/config/providers/route.ts +105 -0
  20. package/app/api/config/route.ts +115 -0
  21. package/app/api/config/status/route.ts +56 -0
  22. package/app/api/config/test/route.ts +212 -0
  23. package/app/api/documents/[id]/route.ts +88 -0
  24. package/app/api/documents/route.ts +53 -0
  25. package/app/api/health/route.ts +32 -0
  26. package/app/api/knowledge/[id]/route.ts +152 -0
  27. package/app/api/knowledge/from-session/route.ts +27 -0
  28. package/app/api/knowledge/route.ts +100 -0
  29. package/app/api/market/knowledge/[id]/route.ts +92 -0
  30. package/app/api/market/knowledge/route.ts +130 -0
  31. package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
  32. package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
  33. package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
  34. package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
  35. package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
  36. package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
  37. package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
  38. package/app/api/marketplace/skills/[id]/route.ts +177 -0
  39. package/app/api/marketplace/skills/route.ts +235 -0
  40. package/app/api/memory/route.ts +40 -0
  41. package/app/api/my/files/[id]/route.ts +52 -0
  42. package/app/api/my/files/route.ts +230 -0
  43. package/app/api/my/knowledge/route.ts +36 -0
  44. package/app/api/pi-chat/route.ts +443 -0
  45. package/app/api/recommend/route.ts +38 -0
  46. package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
  47. package/app/api/scheduled-tasks/[id]/route.ts +165 -0
  48. package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
  49. package/app/api/scheduled-tasks/route.ts +101 -0
  50. package/app/api/sessions/[id]/messages/route.ts +212 -0
  51. package/app/api/sessions/route.ts +101 -0
  52. package/app/api/share/file/[id]/route.ts +37 -0
  53. package/app/api/skills/[name]/execute/route.ts +121 -0
  54. package/app/api/skills/[name]/route.ts +167 -0
  55. package/app/api/skills/create/route.ts +65 -0
  56. package/app/api/skills/generate/route.ts +405 -0
  57. package/app/api/skills/installed/route.ts +151 -0
  58. package/app/api/skills/route.ts +174 -0
  59. package/app/api/skills/translate/route.ts +40 -0
  60. package/app/api/skills/user/[name]/route.ts +159 -0
  61. package/app/api/skills/user/route.ts +90 -0
  62. package/app/api/statistics/route.ts +94 -0
  63. package/app/api/task-executions/[id]/route.ts +34 -0
  64. package/app/api/task-executions/route.ts +29 -0
  65. package/app/api/tickets/[id]/approve/route.ts +129 -0
  66. package/app/api/tickets/[id]/execute/route.ts +201 -0
  67. package/app/api/tickets/[id]/route.ts +127 -0
  68. package/app/api/tickets/route.ts +103 -0
  69. package/app/api/user/skills/route.ts +175 -0
  70. package/app/api/users/route.ts +80 -0
  71. package/app/chat/page.tsx +5 -0
  72. package/app/globals.css +84 -0
  73. package/app/h5/layout.tsx +5 -0
  74. package/app/h5/mobile-approvals-page.tsx +167 -0
  75. package/app/h5/mobile-chat-page.tsx +951 -0
  76. package/app/h5/mobile-profile-page.tsx +147 -0
  77. package/app/h5/mobile-tickets-page.tsx +121 -0
  78. package/app/h5/page.tsx +23 -0
  79. package/app/h5/ticket-action-buttons.tsx +80 -0
  80. package/app/layout.tsx +26 -0
  81. package/app/login/page.tsx +318 -0
  82. package/app/market/knowledge/[id]/page.tsx +77 -0
  83. package/app/market/knowledge/page.tsx +358 -0
  84. package/app/market/layout.tsx +29 -0
  85. package/app/market/page.tsx +18 -0
  86. package/app/market/skills/page.tsx +397 -0
  87. package/app/my/files/page.tsx +511 -0
  88. package/app/my/knowledge/[id]/page.tsx +271 -0
  89. package/app/my/knowledge/new/page.tsx +234 -0
  90. package/app/my/knowledge/page.tsx +248 -0
  91. package/app/my/layout.tsx +32 -0
  92. package/app/my/memory/page.tsx +164 -0
  93. package/app/my/page.tsx +18 -0
  94. package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
  95. package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
  96. package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
  97. package/app/my/scheduled-tasks/new/page.tsx +230 -0
  98. package/app/my/scheduled-tasks/page.tsx +27 -0
  99. package/app/my/skills/[name]/page.tsx +320 -0
  100. package/app/my/skills/new/page.tsx +394 -0
  101. package/app/my/skills/page.tsx +303 -0
  102. package/app/page.tsx +2288 -0
  103. package/app/share/[sessionId]/page.tsx +226 -0
  104. package/app/share/file/[id]/page.tsx +140 -0
  105. package/bin/README.md +63 -0
  106. package/bin/generate-api-system +300 -0
  107. package/bin/postinstall.js +95 -0
  108. package/bin/work-agent.js +173 -0
  109. package/components/ai-elements/agent.tsx +142 -0
  110. package/components/ai-elements/artifact.tsx +149 -0
  111. package/components/ai-elements/attachments.tsx +427 -0
  112. package/components/ai-elements/audio-player.tsx +232 -0
  113. package/components/ai-elements/canvas.tsx +26 -0
  114. package/components/ai-elements/chain-of-thought.tsx +223 -0
  115. package/components/ai-elements/checkpoint.tsx +72 -0
  116. package/components/ai-elements/code-block.tsx +555 -0
  117. package/components/ai-elements/commit.tsx +449 -0
  118. package/components/ai-elements/confirmation.tsx +173 -0
  119. package/components/ai-elements/connection.tsx +28 -0
  120. package/components/ai-elements/context.tsx +410 -0
  121. package/components/ai-elements/controls.tsx +19 -0
  122. package/components/ai-elements/conversation.tsx +167 -0
  123. package/components/ai-elements/edge.tsx +144 -0
  124. package/components/ai-elements/environment-variables.tsx +325 -0
  125. package/components/ai-elements/file-tree.tsx +298 -0
  126. package/components/ai-elements/image.tsx +25 -0
  127. package/components/ai-elements/inline-citation.tsx +294 -0
  128. package/components/ai-elements/jsx-preview.tsx +250 -0
  129. package/components/ai-elements/message.tsx +367 -0
  130. package/components/ai-elements/mic-selector.tsx +372 -0
  131. package/components/ai-elements/model-selector.tsx +214 -0
  132. package/components/ai-elements/node.tsx +72 -0
  133. package/components/ai-elements/open-in-chat.tsx +367 -0
  134. package/components/ai-elements/package-info.tsx +235 -0
  135. package/components/ai-elements/panel.tsx +16 -0
  136. package/components/ai-elements/persona.tsx +280 -0
  137. package/components/ai-elements/plan.tsx +144 -0
  138. package/components/ai-elements/prompt-input.tsx +1341 -0
  139. package/components/ai-elements/queue.tsx +275 -0
  140. package/components/ai-elements/reasoning.tsx +355 -0
  141. package/components/ai-elements/sandbox.tsx +133 -0
  142. package/components/ai-elements/schema-display.tsx +473 -0
  143. package/components/ai-elements/shimmer.tsx +78 -0
  144. package/components/ai-elements/snippet.tsx +141 -0
  145. package/components/ai-elements/sources.tsx +78 -0
  146. package/components/ai-elements/speech-input.tsx +324 -0
  147. package/components/ai-elements/stack-trace.tsx +531 -0
  148. package/components/ai-elements/suggestion.tsx +58 -0
  149. package/components/ai-elements/task.tsx +88 -0
  150. package/components/ai-elements/terminal.tsx +277 -0
  151. package/components/ai-elements/test-results.tsx +497 -0
  152. package/components/ai-elements/tool.tsx +174 -0
  153. package/components/ai-elements/toolbar.tsx +17 -0
  154. package/components/ai-elements/transcription.tsx +126 -0
  155. package/components/ai-elements/voice-selector.tsx +525 -0
  156. package/components/ai-elements/web-preview.tsx +282 -0
  157. package/components/audit-log-list.tsx +114 -0
  158. package/components/chat/EmptyPreviewState.tsx +12 -0
  159. package/components/chat/KnowledgePickerDialog.tsx +464 -0
  160. package/components/chat/KnowledgePreview.tsx +70 -0
  161. package/components/chat/KnowledgePreviewPanel.tsx +86 -0
  162. package/components/chat/MentionInput.tsx +309 -0
  163. package/components/chat/OrganizeDialog.tsx +258 -0
  164. package/components/chat/RecommendationBanner.tsx +94 -0
  165. package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
  166. package/components/chat/SkillSelector.tsx +305 -0
  167. package/components/chat/SkillSwitcher.tsx +163 -0
  168. package/components/client-layout.tsx +15 -0
  169. package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
  170. package/components/layout-wrapper.tsx +18 -0
  171. package/components/mobile-layout.tsx +62 -0
  172. package/components/scheduled-task-list.tsx +356 -0
  173. package/components/setup-guide.tsx +484 -0
  174. package/components/sub-nav.tsx +54 -0
  175. package/components/ticket-detail-content.tsx +383 -0
  176. package/components/ticket-list.tsx +366 -0
  177. package/components/top-nav.tsx +132 -0
  178. package/components/ui/accordion.tsx +58 -0
  179. package/components/ui/alert.tsx +59 -0
  180. package/components/ui/avatar.tsx +50 -0
  181. package/components/ui/badge.tsx +36 -0
  182. package/components/ui/button-group.tsx +83 -0
  183. package/components/ui/button.tsx +57 -0
  184. package/components/ui/card.tsx +91 -0
  185. package/components/ui/carousel.tsx +262 -0
  186. package/components/ui/collapsible.tsx +11 -0
  187. package/components/ui/command.tsx +153 -0
  188. package/components/ui/dialog.tsx +122 -0
  189. package/components/ui/dropdown-menu.tsx +200 -0
  190. package/components/ui/hover-card.tsx +29 -0
  191. package/components/ui/input-group.tsx +170 -0
  192. package/components/ui/input.tsx +22 -0
  193. package/components/ui/label.tsx +26 -0
  194. package/components/ui/popover.tsx +31 -0
  195. package/components/ui/progress.tsx +28 -0
  196. package/components/ui/scroll-area.tsx +48 -0
  197. package/components/ui/select.tsx +174 -0
  198. package/components/ui/separator.tsx +31 -0
  199. package/components/ui/spinner.tsx +16 -0
  200. package/components/ui/switch.tsx +29 -0
  201. package/components/ui/table.tsx +120 -0
  202. package/components/ui/tabs.tsx +55 -0
  203. package/components/ui/textarea.tsx +22 -0
  204. package/components/ui/tooltip.tsx +30 -0
  205. package/components/welcome-guide.tsx +182 -0
  206. package/components.json +24 -0
  207. package/lib/command-parser.ts +331 -0
  208. package/lib/dangerous-commands.ts +672 -0
  209. package/lib/db.ts +2250 -0
  210. package/lib/feishu-auth.ts +135 -0
  211. package/lib/file-storage.ts +306 -0
  212. package/lib/file-tool.ts +583 -0
  213. package/lib/knowledge-tool.ts +152 -0
  214. package/lib/knowledge-types.ts +66 -0
  215. package/lib/market-client.ts +313 -0
  216. package/lib/market-db.ts +736 -0
  217. package/lib/market-types.ts +51 -0
  218. package/lib/memory-tool.ts +211 -0
  219. package/lib/memory.ts +197 -0
  220. package/lib/pi-config.ts +436 -0
  221. package/lib/pi-session.ts +799 -0
  222. package/lib/pinyin.ts +13 -0
  223. package/lib/recommendation.ts +227 -0
  224. package/lib/risk-estimator.ts +350 -0
  225. package/lib/scheduled-task-tool.ts +184 -0
  226. package/lib/scheduler-init.ts +43 -0
  227. package/lib/scheduler.ts +416 -0
  228. package/lib/secure-bash-tool.ts +413 -0
  229. package/lib/skill-engine.ts +396 -0
  230. package/lib/skill-generator.ts +269 -0
  231. package/lib/skill-loader.ts +234 -0
  232. package/lib/skill-tool.ts +188 -0
  233. package/lib/skill-types.ts +82 -0
  234. package/lib/skills-init.ts +58 -0
  235. package/lib/ticket-tool.ts +246 -0
  236. package/lib/user-skill-types.ts +30 -0
  237. package/lib/user-skills.ts +362 -0
  238. package/lib/utils.ts +6 -0
  239. package/lib/workflow.ts +154 -0
  240. package/lib/zip-tool.ts +191 -0
  241. package/next.config.js +8 -0
  242. package/package.json +106 -0
  243. package/public/.gitkeep +1 -0
  244. package/public/icon.svg +1 -0
  245. package/tsconfig.json +42 -0
@@ -0,0 +1,555 @@
1
+ "use client";
2
+
3
+ import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";
4
+ import type {
5
+ BundledLanguage,
6
+ BundledTheme,
7
+ HighlighterGeneric,
8
+ ThemedToken,
9
+ } from "shiki";
10
+
11
+ import { Button } from "@/components/ui/button";
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from "@/components/ui/select";
19
+ import { cn } from "@/lib/utils";
20
+ import { CheckIcon, CopyIcon } from "lucide-react";
21
+ import {
22
+ createContext,
23
+ memo,
24
+ useCallback,
25
+ useContext,
26
+ useEffect,
27
+ useMemo,
28
+ useRef,
29
+ useState,
30
+ } from "react";
31
+ import { createHighlighter } from "shiki";
32
+
33
+ // Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline
34
+ // biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check
35
+ // eslint-disable-next-line no-bitwise -- shiki bitflag check
36
+ const isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;
37
+ // biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check
38
+ // eslint-disable-next-line no-bitwise -- shiki bitflag check
39
+ // oxlint-disable-next-line eslint(no-bitwise)
40
+ const isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;
41
+ const isUnderline = (fontStyle: number | undefined) =>
42
+ // biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check
43
+ // oxlint-disable-next-line eslint(no-bitwise)
44
+ fontStyle && fontStyle & 4;
45
+
46
+ // Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint
47
+ interface KeyedToken {
48
+ token: ThemedToken;
49
+ key: string;
50
+ }
51
+ interface KeyedLine {
52
+ tokens: KeyedToken[];
53
+ key: string;
54
+ }
55
+
56
+ const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] =>
57
+ lines.map((line, lineIdx) => ({
58
+ key: `line-${lineIdx}`,
59
+ tokens: line.map((token, tokenIdx) => ({
60
+ key: `line-${lineIdx}-${tokenIdx}`,
61
+ token,
62
+ })),
63
+ }));
64
+
65
+ // Token rendering component
66
+ const TokenSpan = ({ token }: { token: ThemedToken }) => (
67
+ <span
68
+ className="dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)]"
69
+ style={
70
+ {
71
+ backgroundColor: token.bgColor,
72
+ color: token.color,
73
+ fontStyle: isItalic(token.fontStyle) ? "italic" : undefined,
74
+ fontWeight: isBold(token.fontStyle) ? "bold" : undefined,
75
+ textDecoration: isUnderline(token.fontStyle) ? "underline" : undefined,
76
+ ...token.htmlStyle,
77
+ } as CSSProperties
78
+ }
79
+ >
80
+ {token.content}
81
+ </span>
82
+ );
83
+
84
+ // Line rendering component
85
+ const LineSpan = ({
86
+ keyedLine,
87
+ showLineNumbers,
88
+ }: {
89
+ keyedLine: KeyedLine;
90
+ showLineNumbers: boolean;
91
+ }) => (
92
+ <span className={showLineNumbers ? LINE_NUMBER_CLASSES : "block"}>
93
+ {keyedLine.tokens.length === 0
94
+ ? "\n"
95
+ : keyedLine.tokens.map(({ token, key }) => (
96
+ <TokenSpan key={key} token={token} />
97
+ ))}
98
+ </span>
99
+ );
100
+
101
+ // Types
102
+ type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
103
+ code: string;
104
+ language: BundledLanguage;
105
+ showLineNumbers?: boolean;
106
+ };
107
+
108
+ interface TokenizedCode {
109
+ tokens: ThemedToken[][];
110
+ fg: string;
111
+ bg: string;
112
+ }
113
+
114
+ interface CodeBlockContextType {
115
+ code: string;
116
+ }
117
+
118
+ // Context
119
+ const CodeBlockContext = createContext<CodeBlockContextType>({
120
+ code: "",
121
+ });
122
+
123
+ // Highlighter cache (singleton per language)
124
+ const highlighterCache = new Map<
125
+ string,
126
+ Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>
127
+ >();
128
+
129
+ // Token cache
130
+ const tokensCache = new Map<string, TokenizedCode>();
131
+
132
+ // Subscribers for async token updates
133
+ const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();
134
+
135
+ const getTokensCacheKey = (code: string, language: BundledLanguage) => {
136
+ const start = code.slice(0, 100);
137
+ const end = code.length > 100 ? code.slice(-100) : "";
138
+ return `${language}:${code.length}:${start}:${end}`;
139
+ };
140
+
141
+ const getHighlighter = (
142
+ language: BundledLanguage
143
+ ): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {
144
+ const cached = highlighterCache.get(language);
145
+ if (cached) {
146
+ return cached;
147
+ }
148
+
149
+ const highlighterPromise = createHighlighter({
150
+ langs: [language],
151
+ themes: ["github-light", "github-dark"],
152
+ });
153
+
154
+ highlighterCache.set(language, highlighterPromise);
155
+ return highlighterPromise;
156
+ };
157
+
158
+ // Create raw tokens for immediate display while highlighting loads
159
+ const createRawTokens = (code: string): TokenizedCode => ({
160
+ bg: "transparent",
161
+ fg: "inherit",
162
+ tokens: code.split("\n").map((line) =>
163
+ line === ""
164
+ ? []
165
+ : [
166
+ {
167
+ color: "inherit",
168
+ content: line,
169
+ } as ThemedToken,
170
+ ]
171
+ ),
172
+ });
173
+
174
+ // Synchronous highlight with callback for async results
175
+ export const highlightCode = (
176
+ code: string,
177
+ language: BundledLanguage,
178
+ // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)
179
+ callback?: (result: TokenizedCode) => void
180
+ ): TokenizedCode | null => {
181
+ const tokensCacheKey = getTokensCacheKey(code, language);
182
+
183
+ // Return cached result if available
184
+ const cached = tokensCache.get(tokensCacheKey);
185
+ if (cached) {
186
+ return cached;
187
+ }
188
+
189
+ // Subscribe callback if provided
190
+ if (callback) {
191
+ if (!subscribers.has(tokensCacheKey)) {
192
+ subscribers.set(tokensCacheKey, new Set());
193
+ }
194
+ subscribers.get(tokensCacheKey)?.add(callback);
195
+ }
196
+
197
+ // Start highlighting in background - fire-and-forget async pattern
198
+ getHighlighter(language)
199
+ // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)
200
+ .then((highlighter) => {
201
+ const availableLangs = highlighter.getLoadedLanguages();
202
+ const langToUse = availableLangs.includes(language) ? language : "text";
203
+
204
+ const result = highlighter.codeToTokens(code, {
205
+ lang: langToUse,
206
+ themes: {
207
+ dark: "github-dark",
208
+ light: "github-light",
209
+ },
210
+ });
211
+
212
+ const tokenized: TokenizedCode = {
213
+ bg: result.bg ?? "transparent",
214
+ fg: result.fg ?? "inherit",
215
+ tokens: result.tokens,
216
+ };
217
+
218
+ // Cache the result
219
+ tokensCache.set(tokensCacheKey, tokenized);
220
+
221
+ // Notify all subscribers
222
+ const subs = subscribers.get(tokensCacheKey);
223
+ if (subs) {
224
+ for (const sub of subs) {
225
+ sub(tokenized);
226
+ }
227
+ subscribers.delete(tokensCacheKey);
228
+ }
229
+ })
230
+ // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks)
231
+ .catch((error) => {
232
+ console.error("Failed to highlight code:", error);
233
+ subscribers.delete(tokensCacheKey);
234
+ });
235
+
236
+ return null;
237
+ };
238
+
239
+ // Line number styles using CSS counters
240
+ const LINE_NUMBER_CLASSES = cn(
241
+ "block",
242
+ "before:content-[counter(line)]",
243
+ "before:inline-block",
244
+ "before:[counter-increment:line]",
245
+ "before:w-8",
246
+ "before:mr-4",
247
+ "before:text-right",
248
+ "before:text-muted-foreground/50",
249
+ "before:font-mono",
250
+ "before:select-none"
251
+ );
252
+
253
+ const CodeBlockBody = memo(
254
+ ({
255
+ tokenized,
256
+ showLineNumbers,
257
+ className,
258
+ }: {
259
+ tokenized: TokenizedCode;
260
+ showLineNumbers: boolean;
261
+ className?: string;
262
+ }) => {
263
+ const preStyle = useMemo(
264
+ () => ({
265
+ backgroundColor: tokenized.bg,
266
+ color: tokenized.fg,
267
+ }),
268
+ [tokenized.bg, tokenized.fg]
269
+ );
270
+
271
+ const keyedLines = useMemo(
272
+ () => addKeysToTokens(tokenized.tokens),
273
+ [tokenized.tokens]
274
+ );
275
+
276
+ return (
277
+ <pre
278
+ className={cn(
279
+ "dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)] m-0 p-4 text-sm",
280
+ className
281
+ )}
282
+ style={preStyle}
283
+ >
284
+ <code
285
+ className={cn(
286
+ "font-mono text-sm",
287
+ showLineNumbers && "[counter-increment:line_0] [counter-reset:line]"
288
+ )}
289
+ >
290
+ {keyedLines.map((keyedLine) => (
291
+ <LineSpan
292
+ key={keyedLine.key}
293
+ keyedLine={keyedLine}
294
+ showLineNumbers={showLineNumbers}
295
+ />
296
+ ))}
297
+ </code>
298
+ </pre>
299
+ );
300
+ },
301
+ (prevProps, nextProps) =>
302
+ prevProps.tokenized === nextProps.tokenized &&
303
+ prevProps.showLineNumbers === nextProps.showLineNumbers &&
304
+ prevProps.className === nextProps.className
305
+ );
306
+
307
+ CodeBlockBody.displayName = "CodeBlockBody";
308
+
309
+ export const CodeBlockContainer = ({
310
+ className,
311
+ language,
312
+ style,
313
+ ...props
314
+ }: HTMLAttributes<HTMLDivElement> & { language: string }) => (
315
+ <div
316
+ className={cn(
317
+ "group relative w-full overflow-hidden rounded-md border bg-background text-foreground",
318
+ className
319
+ )}
320
+ data-language={language}
321
+ style={{
322
+ containIntrinsicSize: "auto 200px",
323
+ contentVisibility: "auto",
324
+ ...style,
325
+ }}
326
+ {...props}
327
+ />
328
+ );
329
+
330
+ export const CodeBlockHeader = ({
331
+ children,
332
+ className,
333
+ ...props
334
+ }: HTMLAttributes<HTMLDivElement>) => (
335
+ <div
336
+ className={cn(
337
+ "flex items-center justify-between border-b bg-muted/80 px-3 py-2 text-muted-foreground text-xs",
338
+ className
339
+ )}
340
+ {...props}
341
+ >
342
+ {children}
343
+ </div>
344
+ );
345
+
346
+ export const CodeBlockTitle = ({
347
+ children,
348
+ className,
349
+ ...props
350
+ }: HTMLAttributes<HTMLDivElement>) => (
351
+ <div className={cn("flex items-center gap-2", className)} {...props}>
352
+ {children}
353
+ </div>
354
+ );
355
+
356
+ export const CodeBlockFilename = ({
357
+ children,
358
+ className,
359
+ ...props
360
+ }: HTMLAttributes<HTMLSpanElement>) => (
361
+ <span className={cn("font-mono", className)} {...props}>
362
+ {children}
363
+ </span>
364
+ );
365
+
366
+ export const CodeBlockActions = ({
367
+ children,
368
+ className,
369
+ ...props
370
+ }: HTMLAttributes<HTMLDivElement>) => (
371
+ <div
372
+ className={cn("-my-1 -mr-1 flex items-center gap-2", className)}
373
+ {...props}
374
+ >
375
+ {children}
376
+ </div>
377
+ );
378
+
379
+ export const CodeBlockContent = ({
380
+ code,
381
+ language,
382
+ showLineNumbers = false,
383
+ }: {
384
+ code: string;
385
+ language: BundledLanguage;
386
+ showLineNumbers?: boolean;
387
+ }) => {
388
+ // Memoized raw tokens for immediate display
389
+ const rawTokens = useMemo(() => createRawTokens(code), [code]);
390
+
391
+ // Try to get cached result synchronously, otherwise use raw tokens
392
+ const [tokenized, setTokenized] = useState<TokenizedCode>(
393
+ () => highlightCode(code, language) ?? rawTokens
394
+ );
395
+
396
+ useEffect(() => {
397
+ let cancelled = false;
398
+
399
+ // Reset to raw tokens when code changes (shows current code, not stale tokens)
400
+ setTokenized(highlightCode(code, language) ?? rawTokens);
401
+
402
+ // Subscribe to async highlighting result
403
+ highlightCode(code, language, (result) => {
404
+ if (!cancelled) {
405
+ setTokenized(result);
406
+ }
407
+ });
408
+
409
+ return () => {
410
+ cancelled = true;
411
+ };
412
+ }, [code, language, rawTokens]);
413
+
414
+ return (
415
+ <div className="relative overflow-auto">
416
+ <CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} />
417
+ </div>
418
+ );
419
+ };
420
+
421
+ export const CodeBlock = ({
422
+ code,
423
+ language,
424
+ showLineNumbers = false,
425
+ className,
426
+ children,
427
+ ...props
428
+ }: CodeBlockProps) => {
429
+ const contextValue = useMemo(() => ({ code }), [code]);
430
+
431
+ return (
432
+ <CodeBlockContext.Provider value={contextValue}>
433
+ <CodeBlockContainer className={className} language={language} {...props}>
434
+ {children}
435
+ <CodeBlockContent
436
+ code={code}
437
+ language={language}
438
+ showLineNumbers={showLineNumbers}
439
+ />
440
+ </CodeBlockContainer>
441
+ </CodeBlockContext.Provider>
442
+ );
443
+ };
444
+
445
+ export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
446
+ onCopy?: () => void;
447
+ onError?: (error: Error) => void;
448
+ timeout?: number;
449
+ };
450
+
451
+ export const CodeBlockCopyButton = ({
452
+ onCopy,
453
+ onError,
454
+ timeout = 2000,
455
+ children,
456
+ className,
457
+ ...props
458
+ }: CodeBlockCopyButtonProps) => {
459
+ const [isCopied, setIsCopied] = useState(false);
460
+ const timeoutRef = useRef<number>(0);
461
+ const { code } = useContext(CodeBlockContext);
462
+
463
+ const copyToClipboard = useCallback(async () => {
464
+ if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
465
+ onError?.(new Error("Clipboard API not available"));
466
+ return;
467
+ }
468
+
469
+ try {
470
+ if (!isCopied) {
471
+ await navigator.clipboard.writeText(code);
472
+ setIsCopied(true);
473
+ onCopy?.();
474
+ timeoutRef.current = window.setTimeout(
475
+ () => setIsCopied(false),
476
+ timeout
477
+ );
478
+ }
479
+ } catch (error) {
480
+ onError?.(error as Error);
481
+ }
482
+ }, [code, onCopy, onError, timeout, isCopied]);
483
+
484
+ useEffect(
485
+ () => () => {
486
+ window.clearTimeout(timeoutRef.current);
487
+ },
488
+ []
489
+ );
490
+
491
+ const Icon = isCopied ? CheckIcon : CopyIcon;
492
+
493
+ return (
494
+ <Button
495
+ className={cn("shrink-0", className)}
496
+ onClick={copyToClipboard}
497
+ size="icon"
498
+ variant="ghost"
499
+ {...props}
500
+ >
501
+ {children ?? <Icon size={14} />}
502
+ </Button>
503
+ );
504
+ };
505
+
506
+ export type CodeBlockLanguageSelectorProps = ComponentProps<typeof Select>;
507
+
508
+ export const CodeBlockLanguageSelector = (
509
+ props: CodeBlockLanguageSelectorProps
510
+ ) => <Select {...props} />;
511
+
512
+ export type CodeBlockLanguageSelectorTriggerProps = ComponentProps<
513
+ typeof SelectTrigger
514
+ >;
515
+
516
+ export const CodeBlockLanguageSelectorTrigger = ({
517
+ className,
518
+ ...props
519
+ }: CodeBlockLanguageSelectorTriggerProps) => (
520
+ <SelectTrigger
521
+ className={cn(
522
+ "h-7 border-none bg-transparent px-2 text-xs shadow-none",
523
+ className
524
+ )}
525
+ size="sm"
526
+ {...props}
527
+ />
528
+ );
529
+
530
+ export type CodeBlockLanguageSelectorValueProps = ComponentProps<
531
+ typeof SelectValue
532
+ >;
533
+
534
+ export const CodeBlockLanguageSelectorValue = (
535
+ props: CodeBlockLanguageSelectorValueProps
536
+ ) => <SelectValue {...props} />;
537
+
538
+ export type CodeBlockLanguageSelectorContentProps = ComponentProps<
539
+ typeof SelectContent
540
+ >;
541
+
542
+ export const CodeBlockLanguageSelectorContent = ({
543
+ align = "end",
544
+ ...props
545
+ }: CodeBlockLanguageSelectorContentProps) => (
546
+ <SelectContent align={align} {...props} />
547
+ );
548
+
549
+ export type CodeBlockLanguageSelectorItemProps = ComponentProps<
550
+ typeof SelectItem
551
+ >;
552
+
553
+ export const CodeBlockLanguageSelectorItem = (
554
+ props: CodeBlockLanguageSelectorItemProps
555
+ ) => <SelectItem {...props} />;