@dyyz1993/create-agent 2.0.0 → 2.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 (165) hide show
  1. package/package.json +1 -1
  2. package/src/commands/create.ts +31 -31
  3. package/src/commands/workspace.ts +112 -105
  4. package/src/lib/copy.ts +24 -1
  5. package/templates/agent/electron/main.js +46 -0
  6. package/templates/agent/electron/preload.js +5 -0
  7. package/templates/agent/electron-builder.json +40 -0
  8. package/templates/agent/eslint.config.mjs +17 -2
  9. package/templates/agent/package.json +69 -3
  10. package/templates/agent/src/__tests__/hybrid-mode.test.ts +126 -0
  11. package/templates/agent/src/__tests__/server-config-security.test.ts +55 -0
  12. package/templates/agent/src/bun/index.ts +96 -41
  13. package/templates/agent/src/bun/three.d.ts +1 -0
  14. package/templates/agent/src/gateway/__tests__/ws-handler-token.test.ts +118 -0
  15. package/templates/agent/src/gateway/http-routes.ts +1 -1
  16. package/templates/agent/src/gateway/ws-handler.ts +84 -56
  17. package/templates/agent/src/mainview/App.tsx +29 -26
  18. package/templates/agent/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  19. package/templates/agent/src/mainview/__tests__/setup.ts +32 -29
  20. package/templates/agent/src/mainview/__tests__/theme-variables.test.ts +36 -0
  21. package/templates/agent/src/mainview/components/chat/ChatPanel.tsx +88 -88
  22. package/templates/agent/src/mainview/components/common/ErrorBoundary.tsx +1 -58
  23. package/templates/agent/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  24. package/templates/agent/src/mainview/components/common/ThemeToggle.tsx +1 -0
  25. package/templates/agent/src/mainview/components/feed/FeedPanel.tsx +232 -210
  26. package/templates/agent/src/mainview/components/file-preview/VirtualizedCodeView.tsx +105 -81
  27. package/templates/agent/src/mainview/components/search/SearchPanel.tsx +427 -378
  28. package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +149 -122
  29. package/templates/agent/src/mainview/hooks/use-input-history.ts +70 -61
  30. package/templates/agent/src/mainview/lib/api-client.ts +179 -172
  31. package/templates/agent/src/mainview/main.tsx +4 -10
  32. package/templates/agent/src/mainview/stores/use-feed-store.ts +107 -107
  33. package/templates/agent/src/mainview/utils/drop-handler.ts +114 -115
  34. package/templates/agent/src/server-config.ts +34 -32
  35. package/templates/agent/src/server.ts +42 -54
  36. package/templates/agent/src/shared/handlers/__tests__/chat-concurrency.test.ts +127 -0
  37. package/templates/agent/src/shared/handlers/__tests__/handler-isolation.test.ts +244 -0
  38. package/templates/agent/src/shared/handlers/bash.ts +65 -65
  39. package/templates/agent/src/shared/handlers/chat.ts +189 -155
  40. package/templates/agent/src/shared/handlers/debug.ts +12 -0
  41. package/templates/agent/src/shared/handlers/feed.ts +31 -32
  42. package/templates/agent/src/shared/handlers/git.ts +286 -243
  43. package/templates/agent/src/shared/handlers/index.ts +1 -0
  44. package/templates/agent/src/shared/handlers/rules.ts +31 -31
  45. package/templates/agent/src/shared/handlers/todo.ts +31 -31
  46. package/templates/agent/src/shared/http-routes.ts +250 -0
  47. package/templates/agent/src/shared/lib/__tests__/web-server.test.ts +190 -0
  48. package/templates/agent/src/shared/lib/bash-security.ts +43 -43
  49. package/templates/agent/src/shared/lib/logger.ts +117 -95
  50. package/templates/agent/src/shared/lib/web-server.ts +128 -0
  51. package/templates/agent/src/shared/modules/debug.ts +8 -0
  52. package/templates/agent/src/shared/rpc-schema.ts +16 -3
  53. package/templates/agent/test-upload.txt +0 -0
  54. package/templates/agent/tsconfig.ipc.json +14 -0
  55. package/templates/agent/tsconfig.json +13 -3
  56. package/templates/chat/eslint.config.mjs +15 -2
  57. package/templates/chat/package.json +13 -2
  58. package/templates/chat/src/__tests__/hybrid-mode.test.ts +125 -0
  59. package/templates/chat/src/__tests__/server-config-security.test.ts +55 -0
  60. package/templates/chat/src/__tests__/server-config.test.ts +46 -44
  61. package/templates/chat/src/bun/index.ts +94 -44
  62. package/templates/chat/src/bun/three.d.ts +1 -0
  63. package/templates/chat/src/gateway/http-routes.ts +1 -1
  64. package/templates/chat/src/gateway/ws-handler.ts +84 -56
  65. package/templates/chat/src/mainview/App.tsx +6 -0
  66. package/templates/chat/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  67. package/templates/chat/src/mainview/__tests__/theme-variables.test.ts +36 -0
  68. package/templates/chat/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  69. package/templates/chat/src/mainview/components/common/ThemeToggle.tsx +1 -0
  70. package/templates/chat/src/mainview/hooks/use-input-history.ts +70 -61
  71. package/templates/chat/src/mainview/lib/api-client.ts +180 -156
  72. package/templates/chat/src/mainview/lib/rpc-cache.ts +84 -0
  73. package/templates/chat/src/mainview/main.tsx +10 -6
  74. package/templates/chat/src/server-config.ts +33 -31
  75. package/templates/chat/src/server.ts +42 -60
  76. package/templates/chat/src/shared/handlers/chat.ts +196 -162
  77. package/templates/chat/src/shared/handlers/debug.ts +12 -0
  78. package/templates/chat/src/shared/handlers/index.ts +1 -0
  79. package/templates/chat/src/shared/http-routes.ts +250 -0
  80. package/templates/chat/src/shared/lib/__tests__/logger.test.ts +92 -0
  81. package/templates/chat/src/shared/lib/logger.ts +108 -95
  82. package/templates/chat/src/shared/lib/web-server.ts +128 -0
  83. package/templates/chat/src/shared/modules/debug.ts +8 -0
  84. package/templates/chat/src/shared/rpc-schema.ts +3 -2
  85. package/templates/chat/test-upload.txt +0 -0
  86. package/templates/chat/tsconfig.ipc.json +14 -0
  87. package/templates/chat/tsconfig.json +12 -2
  88. package/templates/general/eslint.config.mjs +15 -2
  89. package/templates/general/package.json +13 -2
  90. package/templates/general/src/__tests__/hybrid-mode.test.ts +125 -0
  91. package/templates/general/src/__tests__/server-config-security.test.ts +55 -0
  92. package/templates/general/src/__tests__/server-config.test.ts +46 -44
  93. package/templates/general/src/bun/index.ts +94 -44
  94. package/templates/general/src/bun/three.d.ts +1 -0
  95. package/templates/general/src/gateway/http-routes.ts +1 -1
  96. package/templates/general/src/gateway/ws-handler.ts +84 -56
  97. package/templates/general/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  98. package/templates/general/src/mainview/__tests__/theme-variables.test.ts +36 -0
  99. package/templates/general/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  100. package/templates/general/src/mainview/components/common/ThemeToggle.tsx +1 -0
  101. package/templates/general/src/mainview/components/feed/FeedPanel.tsx +241 -219
  102. package/templates/general/src/mainview/components/file-preview/VirtualizedCodeView.tsx +101 -81
  103. package/templates/general/src/mainview/components/layout/AppLayout.tsx +6 -0
  104. package/templates/general/src/mainview/components/search/SearchPanel.tsx +429 -378
  105. package/templates/general/src/mainview/hooks/use-input-history.ts +70 -61
  106. package/templates/general/src/mainview/lib/api-client.ts +180 -156
  107. package/templates/general/src/mainview/lib/rpc-cache.ts +84 -0
  108. package/templates/general/src/mainview/main.tsx +10 -6
  109. package/templates/general/src/mainview/stores/use-feed-store.ts +107 -107
  110. package/templates/general/src/mainview/utils/drop-handler.ts +114 -115
  111. package/templates/general/src/server-config.ts +33 -31
  112. package/templates/general/src/server.ts +42 -60
  113. package/templates/general/src/shared/handlers/__tests__/chat.test.ts +165 -160
  114. package/templates/general/src/shared/handlers/chat.ts +189 -155
  115. package/templates/general/src/shared/handlers/debug.ts +12 -0
  116. package/templates/general/src/shared/handlers/feed.ts +38 -40
  117. package/templates/general/src/shared/handlers/git.ts +286 -243
  118. package/templates/general/src/shared/handlers/index.ts +1 -0
  119. package/templates/general/src/shared/http-routes.ts +250 -0
  120. package/templates/general/src/shared/lib/__tests__/logger.test.ts +92 -0
  121. package/templates/general/src/shared/lib/logger.ts +117 -95
  122. package/templates/general/src/shared/lib/web-server.ts +128 -0
  123. package/templates/general/src/shared/modules/debug.ts +8 -0
  124. package/templates/general/src/shared/rpc-schema.ts +12 -2
  125. package/templates/general/test-upload.txt +0 -0
  126. package/templates/general/tsconfig.ipc.json +14 -0
  127. package/templates/general/tsconfig.json +12 -2
  128. package/templates/shared/__tests__/cors-security.test.ts +165 -0
  129. package/templates/shared/__tests__/file-path-security.test.ts +260 -0
  130. package/templates/shared/__tests__/http-routes.test.ts +174 -0
  131. package/templates/shared/__tests__/logger.test.ts +186 -0
  132. package/templates/shared/components/ErrorBoundary.tsx +59 -0
  133. package/templates/shared/env.d.ts +11 -0
  134. package/templates/shared/http-routes.ts +250 -0
  135. package/templates/shared/logger.ts +129 -0
  136. package/templates/shared/test-upload.txt +0 -0
  137. package/templates/shared/vite-base.config.ts +92 -0
  138. package/templates/shared/vitest-base.config.ts +42 -0
  139. package/templates/agent/eslint-plugin-rpc/index.js +0 -50
  140. package/templates/agent/eslint-plugin-rpc/package.json +0 -6
  141. package/templates/agent/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  142. package/templates/agent/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  143. package/templates/agent/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  144. package/templates/agent/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  145. package/templates/agent/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  146. package/templates/agent/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  147. package/templates/agent/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  148. package/templates/chat/eslint-plugin-rpc/index.js +0 -50
  149. package/templates/chat/eslint-plugin-rpc/package.json +0 -6
  150. package/templates/chat/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  151. package/templates/chat/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  152. package/templates/chat/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  153. package/templates/chat/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  154. package/templates/chat/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  155. package/templates/chat/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  156. package/templates/chat/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  157. package/templates/general/eslint-plugin-rpc/index.js +0 -50
  158. package/templates/general/eslint-plugin-rpc/package.json +0 -6
  159. package/templates/general/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  160. package/templates/general/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  161. package/templates/general/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  162. package/templates/general/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  163. package/templates/general/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  164. package/templates/general/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  165. package/templates/general/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
@@ -1,41 +1,44 @@
1
+ import type { ReactNode } from "react";
1
2
  import { useState } from "react";
2
3
  import { useConnectionStore } from "./stores/use-connection-store";
3
4
  import { useBreakpointSync } from "./hooks/use-breakpoint";
4
5
  import { useRpcInit } from "./hooks/use-rpc-init";
5
6
  import { useSidebarResize } from "./hooks/use-sidebar-resize";
6
7
  import { AppLayout, type CenterTab } from "./components/layout/AppLayout";
7
- import { ErrorBoundary } from "./components/common/ErrorBoundary";
8
+ import { ErrorBoundary as _EB } from "./components/common/ErrorBoundary";
9
+
10
+ const ErrorBoundary = _EB as unknown as React.FC<{ children: ReactNode }>;
8
11
 
9
12
  function App() {
10
- const ready = useConnectionStore((s) => s.ready);
11
- const [centerTab, setCenterTab] = useState<CenterTab>("chat");
13
+ const ready = useConnectionStore((s) => s.ready);
14
+ const [centerTab, setCenterTab] = useState<CenterTab>("chat");
12
15
 
13
- useBreakpointSync();
14
- useRpcInit();
16
+ useBreakpointSync();
17
+ useRpcInit();
15
18
 
16
- const { sidebarWidth, handleResizeStart } = useSidebarResize();
19
+ const { sidebarWidth, handleResizeStart } = useSidebarResize();
17
20
 
18
- if (!ready) {
19
- return (
20
- <div className="h-screen bg-gray-900 flex items-center justify-center">
21
- <div className="text-center">
22
- <div className="inline-block w-8 h-8 border-2 border-indigo-400 border-t-transparent rounded-full animate-spin mb-4" />
23
- <div className="text-gray-400 text-sm">Connecting to RPC server...</div>
24
- </div>
25
- </div>
26
- );
27
- }
21
+ if (!ready) {
22
+ return (
23
+ <div className="h-screen bg-gray-900 flex items-center justify-center">
24
+ <div className="text-center">
25
+ <div className="inline-block w-8 h-8 border-2 border-indigo-400 border-t-transparent rounded-full animate-spin mb-4" />
26
+ <div className="text-gray-400 text-sm">Connecting to RPC server...</div>
27
+ </div>
28
+ </div>
29
+ );
30
+ }
28
31
 
29
- return (
30
- <ErrorBoundary>
31
- <AppLayout
32
- centerTab={centerTab}
33
- setCenterTab={setCenterTab}
34
- sidebarWidth={sidebarWidth}
35
- handleResizeStart={handleResizeStart}
36
- />
37
- </ErrorBoundary>
38
- );
32
+ return (
33
+ <ErrorBoundary>
34
+ <AppLayout
35
+ centerTab={centerTab}
36
+ setCenterTab={setCenterTab}
37
+ sidebarWidth={sidebarWidth}
38
+ handleResizeStart={handleResizeStart}
39
+ />
40
+ </ErrorBoundary>
41
+ );
39
42
  }
40
43
 
41
44
  export default App;
@@ -44,5 +44,6 @@ describe("i18n configuration", () => {
44
44
  const i18n = await import("../../lib/i18n");
45
45
  await i18n.default.changeLanguage("zh");
46
46
  expect(i18n.default.t("app.title")).toBe("Pi Agent");
47
+ expect(i18n.default.t("sidebar.explorer")).toBe("资源管理器");
47
48
  });
48
49
  });
@@ -1,37 +1,40 @@
1
- import "@testing-library/jest-dom/vitest";
1
+ import { expect } from "vitest";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+
4
+ expect.extend(matchers);
2
5
 
3
6
  Object.defineProperty(window, "matchMedia", {
4
- writable: true,
5
- value: (query: string) => ({
6
- matches: false,
7
- media: query,
8
- onchange: null,
9
- addListener: () => {},
10
- removeListener: () => {},
11
- addEventListener: () => {},
12
- removeEventListener: () => {},
13
- dispatchEvent: () => false,
14
- }),
7
+ writable: true,
8
+ value: (query: string) => ({
9
+ matches: false,
10
+ media: query,
11
+ onchange: null,
12
+ addListener: () => {},
13
+ removeListener: () => {},
14
+ addEventListener: () => {},
15
+ removeEventListener: () => {},
16
+ dispatchEvent: () => false,
17
+ }),
15
18
  });
16
19
 
17
20
  const localStorageMock = (() => {
18
- let store: Record<string, string> = {};
19
- return {
20
- getItem: (key: string) => store[key] ?? null,
21
- setItem: (key: string, value: string) => {
22
- store[key] = String(value);
23
- },
24
- removeItem: (key: string) => {
25
- delete store[key];
26
- },
27
- clear: () => {
28
- store = {};
29
- },
30
- get length() {
31
- return Object.keys(store).length;
32
- },
33
- key: (i: number) => Object.keys(store)[i] ?? null,
34
- };
21
+ let store: Record<string, string> = {};
22
+ return {
23
+ getItem: (key: string) => store[key] ?? null,
24
+ setItem: (key: string, value: string) => {
25
+ store[key] = String(value);
26
+ },
27
+ removeItem: (key: string) => {
28
+ delete store[key];
29
+ },
30
+ clear: () => {
31
+ store = {};
32
+ },
33
+ get length() {
34
+ return Object.keys(store).length;
35
+ },
36
+ key: (i: number) => Object.keys(store)[i] ?? null,
37
+ };
35
38
  })();
36
39
 
37
40
  Object.defineProperty(window, "localStorage", { value: localStorageMock });
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { readFileSync } from "fs";
3
+ import { resolve, dirname } from "path";
4
+
5
+ function extractVarNames(css: string, selector: string): string[] {
6
+ const regex = new RegExp(`${selector.replace(".", "\\.")}\\s*\\{([^}]+)\\}`, "s");
7
+ const match = css.match(regex);
8
+ if (!match) return [];
9
+ const vars = match[1].match(/--[\w-]+/g);
10
+ return vars ? [...new Set(vars)].sort() : [];
11
+ }
12
+
13
+ describe("CSS Variables", () => {
14
+ const cssPath = resolve(dirname(import.meta.url.replace("file://", "")), "../index.css");
15
+ const css = readFileSync(cssPath, "utf-8");
16
+
17
+ it("should have matching variables in :root and .dark", () => {
18
+ const rootVars = extractVarNames(css, ":root");
19
+ const darkVars = extractVarNames(css, ".dark");
20
+
21
+ expect(rootVars.length).toBeGreaterThan(0);
22
+ expect(darkVars.length).toBeGreaterThan(0);
23
+ expect(rootVars).toEqual(darkVars);
24
+ });
25
+
26
+ it("should have all required variable categories", () => {
27
+ const allVars = css.match(/--[\w-]+/g) || [];
28
+ const uniqueVars = [...new Set(allVars)];
29
+
30
+ const requiredPrefixes = ["bg-", "text-", "border-", "accent-"];
31
+ for (const prefix of requiredPrefixes) {
32
+ const hasPrefix = uniqueVars.some((v) => v.includes(prefix));
33
+ expect(hasPrefix, `Missing CSS variables with prefix: ${prefix}`).toBe(true);
34
+ }
35
+ });
36
+ });
@@ -6,97 +6,97 @@ import { useChatStore } from "../../stores/use-chat-store";
6
6
  import { MessageBubble } from "./MessageBubble";
7
7
 
8
8
  export function ChatPanel() {
9
- const { t } = useTranslation();
10
- const messages = useChatStore((s) => s.messages);
11
- const inputText = useChatStore((s) => s.inputText);
12
- const setInputText = useChatStore((s) => s.setInputText);
13
- const sendMessage = useChatStore((s) => s.sendMessage);
14
- const parentRef = useRef<HTMLDivElement>(null);
15
- const prevMessageCountRef = useRef(0);
9
+ const { t } = useTranslation();
10
+ const messages = useChatStore((s) => s.messages);
11
+ const inputText = useChatStore((s) => s.inputText);
12
+ const setInputText = useChatStore((s) => s.setInputText);
13
+ const sendMessage = useChatStore((s) => s.sendMessage);
14
+ const parentRef = useRef<HTMLDivElement>(null);
15
+ const prevMessageCountRef = useRef(0);
16
16
 
17
- const virtualizer = useVirtualizer({
18
- count: messages.length,
19
- getScrollElement: () => parentRef.current,
20
- estimateSize: () => 80,
21
- overscan: 5,
22
- measureElement: (element) => element?.getBoundingClientRect().height ?? 80,
23
- });
17
+ const virtualizer = useVirtualizer({
18
+ count: messages.length,
19
+ getScrollElement: () => parentRef.current,
20
+ estimateSize: () => 80,
21
+ overscan: 5,
22
+ measureElement: (element) => element?.getBoundingClientRect().height ?? 80,
23
+ });
24
24
 
25
- useEffect(() => {
26
- if (messages.length > prevMessageCountRef.current) {
27
- virtualizer.scrollToIndex(messages.length - 1, { align: "end" });
28
- }
29
- prevMessageCountRef.current = messages.length;
30
- }, [messages.length, virtualizer]);
25
+ useEffect(() => {
26
+ if (messages.length > prevMessageCountRef.current) {
27
+ virtualizer.scrollToIndex(messages.length - 1, { align: "end" });
28
+ }
29
+ prevMessageCountRef.current = messages.length;
30
+ }, [messages.length, virtualizer]);
31
31
 
32
- return (
33
- <div className="flex-1 flex flex-col overflow-hidden relative">
34
- <div className="px-4 py-2 bg-[var(--color-bg-secondary)] border-b border-[var(--color-border-primary)] flex items-center justify-between flex-shrink-0">
35
- <h2 className="text-sm font-semibold flex items-center gap-1.5">
36
- <MessageSquare className="w-4 h-4 text-[var(--color-text-accent)]" />
37
- {t("chat.title")}
38
- {messages.length > 0 && (
39
- <span className="ml-1 px-2 py-0.5 bg-[var(--color-accent)]/30 text-[var(--color-text-accent)] rounded text-[10px]">
40
- {messages.length}
41
- </span>
42
- )}
43
- </h2>
44
- </div>
32
+ return (
33
+ <div className="flex-1 flex flex-col overflow-hidden relative">
34
+ <div className="px-4 py-2 bg-[var(--color-bg-secondary)] border-b border-[var(--color-border-primary)] flex items-center justify-between flex-shrink-0">
35
+ <h2 className="text-sm font-semibold flex items-center gap-1.5">
36
+ <MessageSquare className="w-4 h-4 text-[var(--color-text-accent)]" />
37
+ {t("chat.title")}
38
+ {messages.length > 0 && (
39
+ <span className="ml-1 px-2 py-0.5 bg-[var(--color-accent)]/30 text-[var(--color-text-accent)] rounded text-[10px]">
40
+ {messages.length}
41
+ </span>
42
+ )}
43
+ </h2>
44
+ </div>
45
45
 
46
- <div ref={parentRef} className="flex-1 overflow-y-auto">
47
- {messages.length === 0 ? (
48
- <div className="flex items-center justify-center h-full text-[var(--color-text-placeholder)] text-sm">
49
- {t("chat.empty")}
50
- </div>
51
- ) : (
52
- <div
53
- style={{
54
- height: virtualizer.getTotalSize(),
55
- width: "100%",
56
- position: "relative",
57
- }}
58
- >
59
- {virtualizer.getVirtualItems().map((virtualItem) => {
60
- const msg = messages[virtualItem.index];
61
- return (
62
- <div
63
- key={msg.id}
64
- data-index={virtualItem.index}
65
- ref={virtualizer.measureElement}
66
- style={{
67
- position: "absolute",
68
- top: 0,
69
- left: 0,
70
- width: "100%",
71
- transform: `translateY(${virtualItem.start}px)`,
72
- padding: "6px 16px",
73
- }}
74
- >
75
- <MessageBubble message={msg} />
76
- </div>
77
- );
78
- })}
79
- </div>
80
- )}
81
- </div>
46
+ <div ref={parentRef} className="flex-1 overflow-y-auto">
47
+ {messages.length === 0 ? (
48
+ <div className="flex items-center justify-center h-full text-[var(--color-text-placeholder)] text-sm">
49
+ {t("chat.empty")}
50
+ </div>
51
+ ) : (
52
+ <div
53
+ style={{
54
+ height: virtualizer.getTotalSize(),
55
+ width: "100%",
56
+ position: "relative",
57
+ }}
58
+ >
59
+ {virtualizer.getVirtualItems().map((virtualItem) => {
60
+ const msg = messages[virtualItem.index]!;
61
+ return (
62
+ <div
63
+ key={msg.id}
64
+ data-index={virtualItem.index}
65
+ ref={virtualizer.measureElement}
66
+ style={{
67
+ position: "absolute",
68
+ top: 0,
69
+ left: 0,
70
+ width: "100%",
71
+ transform: `translateY(${virtualItem.start}px)`,
72
+ padding: "6px 16px",
73
+ }}
74
+ >
75
+ <MessageBubble message={msg} />
76
+ </div>
77
+ );
78
+ })}
79
+ </div>
80
+ )}
81
+ </div>
82
82
 
83
- <div className="px-4 py-3 bg-[var(--color-bg-secondary)] border-t border-[var(--color-border-primary)] flex gap-2 flex-shrink-0">
84
- <input
85
- type="text"
86
- value={inputText}
87
- onChange={(e) => setInputText(e.target.value)}
88
- onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && sendMessage()}
89
- placeholder={t("chat.placeholder")}
90
- className="flex-1 px-3 py-2 text-sm bg-[var(--color-bg-tertiary)] rounded-lg text-[var(--color-text-primary)] border border-[var(--color-border-secondary)] focus:border-[var(--color-accent)] focus:outline-none"
91
- />
92
- <button
93
- onClick={sendMessage}
94
- className="px-4 py-2 text-sm bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] rounded-lg transition-colors flex items-center gap-1.5"
95
- >
96
- <Send className="w-4 h-4" />
97
- {t("chat.send")}
98
- </button>
99
- </div>
100
- </div>
101
- );
83
+ <div className="px-4 py-3 bg-[var(--color-bg-secondary)] border-t border-[var(--color-border-primary)] flex gap-2 flex-shrink-0">
84
+ <input
85
+ type="text"
86
+ value={inputText}
87
+ onChange={(e) => setInputText(e.target.value)}
88
+ onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && sendMessage()}
89
+ placeholder={t("chat.placeholder")}
90
+ className="flex-1 px-3 py-2 text-sm bg-[var(--color-bg-tertiary)] rounded-lg text-[var(--color-text-primary)] border border-[var(--color-border-secondary)] focus:border-[var(--color-accent)] focus:outline-none"
91
+ />
92
+ <button
93
+ onClick={sendMessage}
94
+ className="px-4 py-2 text-sm bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] rounded-lg transition-colors flex items-center gap-1.5"
95
+ >
96
+ <Send className="w-4 h-4" />
97
+ {t("chat.send")}
98
+ </button>
99
+ </div>
100
+ </div>
101
+ );
102
102
  }
@@ -1,58 +1 @@
1
- import React, { Component, type ErrorInfo, type ReactNode } from "react";
2
-
3
- interface Props {
4
- children: ReactNode;
5
- fallback?: ReactNode;
6
- }
7
-
8
- interface State {
9
- hasError: boolean;
10
- error: Error | null;
11
- }
12
-
13
- export class ErrorBoundary extends Component<Props, State> {
14
- constructor(props: Props) {
15
- super(props);
16
- this.state = { hasError: false, error: null };
17
- }
18
-
19
- static getDerivedStateFromError(error: Error): State {
20
- return { hasError: true, error };
21
- }
22
-
23
- componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
24
- console.error("[ErrorBoundary]", error, errorInfo);
25
- }
26
-
27
- handleRetry = (): void => {
28
- this.setState({ hasError: false, error: null });
29
- };
30
-
31
- render(): ReactNode {
32
- if (this.state.hasError) {
33
- if (this.props.fallback) {
34
- return this.props.fallback;
35
- }
36
-
37
- return (
38
- <div className="flex flex-col items-center justify-center h-full p-8 bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
39
- <div className="text-4xl mb-4">⚠️</div>
40
- <h2 className="text-lg font-semibold mb-2">Something went wrong</h2>
41
- <p className="text-sm text-[var(--color-text-secondary)] mb-4 max-w-md text-center">
42
- {this.state.error?.message || "An unexpected error occurred"}
43
- </p>
44
- <button
45
- onClick={this.handleRetry}
46
- className="px-4 py-2 bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-white rounded-lg transition-colors text-sm"
47
- role="button"
48
- aria-label="Retry"
49
- >
50
- Retry
51
- </button>
52
- </div>
53
- );
54
- }
55
-
56
- return this.props.children;
57
- }
58
- }
1
+ export { ErrorBoundary } from "@shared/components/ErrorBoundary";
@@ -4,6 +4,7 @@ export function LanguageSwitcher() {
4
4
  const { locale, setLocale } = useLocaleStore();
5
5
  return (
6
6
  <button
7
+ data-testid="language-switcher"
7
8
  onClick={() => setLocale(locale === "en" ? "zh" : "en")}
8
9
  className="px-1.5 py-0.5 rounded text-xs hover:bg-[var(--color-bg-hover)] transition-colors text-[var(--color-text-secondary)]"
9
10
  title={locale === "en" ? "切换到中文" : "Switch to English"}
@@ -6,6 +6,7 @@ export function ThemeToggle() {
6
6
 
7
7
  return (
8
8
  <button
9
+ data-testid="theme-toggle"
9
10
  onClick={toggleTheme}
10
11
  className="p-1.5 rounded-md hover:bg-[var(--color-bg-hover)] transition-colors"
11
12
  title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}