@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
@@ -5,244 +5,266 @@ import { apiClient } from "../../lib/api-client";
5
5
  import { useLogStore } from "../../stores/use-log-store";
6
6
  import { useConnectionStore } from "../../stores/use-connection-store";
7
7
  import { useFeedStore, useEventStreamStore, type SubEventType } from "../../stores/use-feed-store";
8
- import type { FeedCategory } from "../../../shared/modules/feed";
8
+ import type { FeedCategory } from "@shared/modules/feed";
9
9
 
10
10
  const CATEGORIES: FeedCategory[] = ["tech", "news", "general"];
11
11
  const EVENT_TYPES: SubEventType[] = ["feed.update", "chat.message", "timer.tick"];
12
12
  const FILTER_PRESETS: Record<SubEventType, string[]> = {
13
- "feed.update": ["", '{"category":"tech"}', '{"category":"news"}', '{"author":"alice"}'],
14
- "chat.message": ["", '{"role":"user"}', '{"role":"assistant"}'],
15
- "timer.tick": ["", '{"channel":"default"}'],
13
+ "feed.update": ["", '{"category":"tech"}', '{"category":"news"}', '{"author":"alice"}'],
14
+ "chat.message": ["", '{"role":"user"}', '{"role":"assistant"}'],
15
+ "timer.tick": ["", '{"channel":"default"}'],
16
16
  };
17
17
 
18
18
  const CATEGORY_COLORS: Record<FeedCategory, string> = {
19
- tech: "bg-blue-600/30 text-blue-300",
20
- news: "bg-amber-600/30 text-amber-300",
21
- general: "bg-green-600/30 text-green-300",
19
+ tech: "bg-blue-600/30 text-blue-300",
20
+ news: "bg-amber-600/30 text-amber-300",
21
+ general: "bg-green-600/30 text-green-300",
22
22
  };
23
23
 
24
24
  export function FeedPanel() {
25
- const { t } = useTranslation();
26
- const posts = useFeedStore((s) => s.posts);
27
- const author = useFeedStore((s) => s.author);
28
- const category = useFeedStore((s) => s.category);
29
- const content = useFeedStore((s) => s.content);
30
- const loading = useFeedStore((s) => s.loading);
31
- const setAuthor = useFeedStore((s) => s.setAuthor);
32
- const setCategory = useFeedStore((s) => s.setCategory);
33
- const setContent = useFeedStore((s) => s.setContent);
34
- const createPost = useFeedStore((s) => s.createPost);
35
- const loadPosts = useFeedStore((s) => s.loadPosts);
36
- const addPost = useFeedStore((s) => s.addPost);
37
- const addLog = useLogStore((s) => s.addLog);
38
- const ready = useConnectionStore((s) => s.ready);
25
+ const { t } = useTranslation();
26
+ const posts = useFeedStore((s) => s.posts);
27
+ const author = useFeedStore((s) => s.author);
28
+ const category = useFeedStore((s) => s.category);
29
+ const content = useFeedStore((s) => s.content);
30
+ const loading = useFeedStore((s) => s.loading);
31
+ const setAuthor = useFeedStore((s) => s.setAuthor);
32
+ const setCategory = useFeedStore((s) => s.setCategory);
33
+ const setContent = useFeedStore((s) => s.setContent);
34
+ const createPost = useFeedStore((s) => s.createPost);
35
+ const loadPosts = useFeedStore((s) => s.loadPosts);
36
+ const addPost = useFeedStore((s) => s.addPost);
37
+ const addLog = useLogStore((s) => s.addLog);
38
+ const ready = useConnectionStore((s) => s.ready);
39
39
 
40
- // Event stream state
41
- const entries = useEventStreamStore((s) => s.entries);
42
- const activeEventType = useEventStreamStore((s) => s.activeEventType);
43
- const activeFilter = useEventStreamStore((s) => s.activeFilter);
44
- const subscribed = useEventStreamStore((s) => s.subscribed);
45
- const setActiveEventType = useEventStreamStore((s) => s.setActiveEventType);
46
- const setActiveFilter = useEventStreamStore((s) => s.setActiveFilter);
47
- const handleSubscribe = useEventStreamStore((s) => s.handleSubscribe);
48
- const handleUnsubscribe = useEventStreamStore((s) => s.handleUnsubscribe);
40
+ // Event stream state
41
+ const entries = useEventStreamStore((s) => s.entries);
42
+ const activeEventType = useEventStreamStore((s) => s.activeEventType);
43
+ const activeFilter = useEventStreamStore((s) => s.activeFilter);
44
+ const subscribed = useEventStreamStore((s) => s.subscribed);
45
+ const setActiveEventType = useEventStreamStore((s) => s.setActiveEventType);
46
+ const setActiveFilter = useEventStreamStore((s) => s.setActiveFilter);
47
+ const handleSubscribe = useEventStreamStore((s) => s.handleSubscribe);
48
+ const handleUnsubscribe = useEventStreamStore((s) => s.handleUnsubscribe);
49
49
 
50
- // On ready: subscribe to feed.update (no filter) + load posts
51
- useEffect(() => {
52
- if (!ready) return;
50
+ // On ready: subscribe to feed.update (no filter) + load posts
51
+ useEffect(() => {
52
+ if (!ready) return;
53
53
 
54
- let subId: string | null = null;
54
+ let subId: string | null = null;
55
55
 
56
- const setup = async () => {
57
- // Subscribe to all feed updates (no filter) to populate feed list
58
- subId = await apiClient.subscribe("feed.update", (payload) => {
59
- addPost(payload as typeof posts[number]);
60
- }, {});
61
- addLog("Subscribed to feed.update (no filter)");
56
+ const setup = async () => {
57
+ // Subscribe to all feed updates (no filter) to populate feed list
58
+ subId = await apiClient.subscribe(
59
+ "feed.update",
60
+ (payload) => {
61
+ addPost(payload as (typeof posts)[number]);
62
+ },
63
+ {}
64
+ );
65
+ addLog("Subscribed to feed.update (no filter)");
62
66
 
63
- await loadPosts();
64
- };
65
- setup();
67
+ await loadPosts();
68
+ };
69
+ setup();
66
70
 
67
- return () => {
68
- if (subId) apiClient.unsubscribe(subId);
69
- };
70
- }, [ready, addLog, loadPosts, addPost]);
71
+ return () => {
72
+ if (subId) apiClient.unsubscribe(subId);
73
+ };
74
+ }, [ready, addLog, loadPosts, addPost]);
71
75
 
72
- return (
73
- <div className="flex-1 flex flex-col overflow-hidden">
74
- {/* Header */}
75
- <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">
76
- <h2 className="text-sm font-semibold flex items-center gap-1.5">
77
- <Rss className="w-4 h-4 text-[var(--color-text-accent)]" />
78
- {t("feed.title")}
79
- {posts.length > 0 && (
80
- <span className="ml-1 px-2 py-0.5 bg-[var(--color-accent)]/30 text-[var(--color-text-accent)] rounded text-[10px]">
81
- {posts.length}
82
- </span>
83
- )}
84
- </h2>
85
- </div>
76
+ return (
77
+ <div className="flex-1 flex flex-col overflow-hidden">
78
+ {/* Header */}
79
+ <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">
80
+ <h2 className="text-sm font-semibold flex items-center gap-1.5">
81
+ <Rss className="w-4 h-4 text-[var(--color-text-accent)]" />
82
+ {t("feed.title")}
83
+ {posts.length > 0 && (
84
+ <span className="ml-1 px-2 py-0.5 bg-[var(--color-accent)]/30 text-[var(--color-text-accent)] rounded text-[10px]">
85
+ {posts.length}
86
+ </span>
87
+ )}
88
+ </h2>
89
+ </div>
86
90
 
87
- <div className="flex-1 overflow-y-auto p-4 space-y-4">
88
- {/* Create Post */}
89
- <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
90
- <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">{t("feed.newPost")}</div>
91
- <div className="flex gap-2">
92
- <input
93
- type="text"
94
- value={author}
95
- onChange={(e) => setAuthor(e.target.value)}
96
- placeholder={t("feed.authorPlaceholder")}
97
- className="w-24 px-2 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)] focus:border-[var(--color-accent)] focus:outline-none"
98
- />
99
- <select
100
- value={category}
101
- onChange={(e) => setCategory(e.target.value as FeedCategory)}
102
- className="px-2 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)]"
103
- >
104
- {CATEGORIES.map((c) => (
105
- <option key={c} value={c}>{c}</option>
106
- ))}
107
- </select>
108
- </div>
109
- <div className="flex gap-2">
110
- <input
111
- type="text"
112
- value={content}
113
- onChange={(e) => setContent(e.target.value)}
114
- onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && createPost()}
115
- placeholder={t("feed.postPlaceholder")}
116
- className="flex-1 px-3 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)] focus:border-[var(--color-accent)] focus:outline-none"
117
- />
118
- <button
119
- onClick={createPost}
120
- disabled={loading || !content.trim()}
121
- className="px-3 py-1.5 text-xs bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 rounded transition-colors flex items-center gap-1"
122
- >
123
- <Send className="w-3 h-3" />
124
- {t("feed.post")}
125
- </button>
126
- </div>
127
- </div>
91
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
92
+ {/* Create Post */}
93
+ <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
94
+ <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">
95
+ {t("feed.newPost")}
96
+ </div>
97
+ <div className="flex gap-2">
98
+ <input
99
+ type="text"
100
+ value={author}
101
+ onChange={(e) => setAuthor(e.target.value)}
102
+ placeholder={t("feed.authorPlaceholder")}
103
+ className="w-24 px-2 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)] focus:border-[var(--color-accent)] focus:outline-none"
104
+ />
105
+ <select
106
+ value={category}
107
+ onChange={(e) => setCategory(e.target.value as FeedCategory)}
108
+ className="px-2 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)]"
109
+ >
110
+ {CATEGORIES.map((c) => (
111
+ <option key={c} value={c}>
112
+ {c}
113
+ </option>
114
+ ))}
115
+ </select>
116
+ </div>
117
+ <div className="flex gap-2">
118
+ <input
119
+ type="text"
120
+ value={content}
121
+ onChange={(e) => setContent(e.target.value)}
122
+ onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && createPost()}
123
+ placeholder={t("feed.postPlaceholder")}
124
+ className="flex-1 px-3 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)] focus:border-[var(--color-accent)] focus:outline-none"
125
+ />
126
+ <button
127
+ onClick={createPost}
128
+ disabled={loading || !content.trim()}
129
+ className="px-3 py-1.5 text-xs bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 rounded transition-colors flex items-center gap-1"
130
+ >
131
+ <Send className="w-3 h-3" />
132
+ {t("feed.post")}
133
+ </button>
134
+ </div>
135
+ </div>
128
136
 
129
- {/* Subscription Panel */}
130
- <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
131
- <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">{t("feed.subscribeWithFilter")}</div>
132
- <div className="flex gap-2 items-center">
133
- <select
134
- value={activeEventType}
135
- onChange={(e) => {
136
- setActiveEventType(e.target.value as SubEventType);
137
- setActiveFilter("");
138
- }}
139
- className="px-2 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)]"
140
- >
141
- {EVENT_TYPES.map((t) => (
142
- <option key={t} value={t}>{t}</option>
143
- ))}
144
- </select>
145
- <input
146
- type="text"
147
- value={activeFilter}
148
- onChange={(e) => setActiveFilter(e.target.value)}
149
- placeholder={t("feed.filterPlaceholder")}
150
- className="flex-1 px-2 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)] focus:border-[var(--color-accent)] focus:outline-none font-mono"
151
- />
152
- {!subscribed ? (
153
- <button
154
- onClick={handleSubscribe}
155
- className="px-3 py-1.5 text-xs bg-green-600 hover:bg-green-700 rounded transition-colors whitespace-nowrap"
156
- >
157
- {t("feed.subscribe")}
158
- </button>
159
- ) : (
160
- <button
161
- onClick={handleUnsubscribe}
162
- className="px-3 py-1.5 text-xs bg-red-600 hover:bg-red-700 rounded transition-colors whitespace-nowrap"
163
- >
164
- {t("feed.unsubscribe")}
165
- </button>
166
- )}
167
- </div>
168
- {/* Filter presets */}
169
- <div className="flex gap-1 flex-wrap">
170
- {FILTER_PRESETS[activeEventType].map((preset) => (
171
- <button
172
- key={preset}
173
- onClick={() => setActiveFilter(preset)}
174
- className={`px-2 py-0.5 text-[10px] rounded border transition-colors ${
175
- activeFilter === preset
176
- ? "border-[var(--color-accent)] bg-[var(--color-accent)]/30 text-[var(--color-text-accent)]"
177
- : "border-[var(--color-border-secondary)] text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]"
178
- }`}
179
- >
180
- {preset || t("feed.noFilter")}
181
- </button>
182
- ))}
183
- </div>
184
- </div>
137
+ {/* Subscription Panel */}
138
+ <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
139
+ <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">
140
+ {t("feed.subscribeWithFilter")}
141
+ </div>
142
+ <div className="flex gap-2 items-center">
143
+ <select
144
+ value={activeEventType}
145
+ onChange={(e) => {
146
+ setActiveEventType(e.target.value as SubEventType);
147
+ setActiveFilter("");
148
+ }}
149
+ className="px-2 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)]"
150
+ >
151
+ {EVENT_TYPES.map((t) => (
152
+ <option key={t} value={t}>
153
+ {t}
154
+ </option>
155
+ ))}
156
+ </select>
157
+ <input
158
+ type="text"
159
+ value={activeFilter}
160
+ onChange={(e) => setActiveFilter(e.target.value)}
161
+ placeholder={t("feed.filterPlaceholder")}
162
+ className="flex-1 px-2 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)] focus:border-[var(--color-accent)] focus:outline-none font-mono"
163
+ />
164
+ {!subscribed ? (
165
+ <button
166
+ onClick={handleSubscribe}
167
+ className="px-3 py-1.5 text-xs bg-green-600 hover:bg-green-700 rounded transition-colors whitespace-nowrap"
168
+ >
169
+ {t("feed.subscribe")}
170
+ </button>
171
+ ) : (
172
+ <button
173
+ onClick={handleUnsubscribe}
174
+ className="px-3 py-1.5 text-xs bg-red-600 hover:bg-red-700 rounded transition-colors whitespace-nowrap"
175
+ >
176
+ {t("feed.unsubscribe")}
177
+ </button>
178
+ )}
179
+ </div>
180
+ {/* Filter presets */}
181
+ <div className="flex gap-1 flex-wrap">
182
+ {FILTER_PRESETS[activeEventType].map((preset) => (
183
+ <button
184
+ key={preset}
185
+ onClick={() => setActiveFilter(preset)}
186
+ className={`px-2 py-0.5 text-[10px] rounded border transition-colors ${
187
+ activeFilter === preset
188
+ ? "border-[var(--color-accent)] bg-[var(--color-accent)]/30 text-[var(--color-text-accent)]"
189
+ : "border-[var(--color-border-secondary)] text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]"
190
+ }`}
191
+ >
192
+ {preset || t("feed.noFilter")}
193
+ </button>
194
+ ))}
195
+ </div>
196
+ </div>
185
197
 
186
- {/* Event Stream */}
187
- {entries.length > 0 && (
188
- <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
189
- <div className="flex items-center justify-between">
190
- <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">
191
- {t("feed.eventStream")}
192
- </div>
193
- {subscribed && (
194
- <span className="px-1.5 py-0.5 bg-green-600/30 text-green-400 rounded text-[10px] animate-pulse">
195
- {t("feed.live")}
196
- </span>
197
- )}
198
- </div>
199
- <div className="max-h-48 overflow-y-auto space-y-1">
200
- {entries.slice(-20).map((entry) => (
201
- <div key={entry.id} className="bg-[var(--color-bg-tertiary)] rounded px-2 py-1.5 text-[11px] font-mono">
202
- <div className="flex items-center gap-2 text-[var(--color-text-tertiary)]">
203
- <span className="text-[var(--color-text-accent)]">{entry.eventType}</span>
204
- <span className="text-[10px]">
205
- {Object.keys(entry.filter).length > 0
206
- ? `filter: ${JSON.stringify(entry.filter)}`
207
- : t("feed.noFilter")}
208
- </span>
209
- <span className="text-[10px] ml-auto">
210
- {new Date(entry.timestamp).toLocaleTimeString()}
211
- </span>
212
- </div>
213
- <pre className="text-cyan-300 mt-0.5 whitespace-pre-wrap break-all">
214
- {JSON.stringify(entry.payload, null, 0)}
215
- </pre>
216
- </div>
217
- ))}
218
- </div>
219
- </div>
220
- )}
198
+ {/* Event Stream */}
199
+ {entries.length > 0 && (
200
+ <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
201
+ <div className="flex items-center justify-between">
202
+ <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">
203
+ {t("feed.eventStream")}
204
+ </div>
205
+ {subscribed && (
206
+ <span className="px-1.5 py-0.5 bg-green-600/30 text-green-400 rounded text-[10px] animate-pulse">
207
+ {t("feed.live")}
208
+ </span>
209
+ )}
210
+ </div>
211
+ <div className="max-h-48 overflow-y-auto space-y-1">
212
+ {entries.slice(-20).map((entry) => (
213
+ <div
214
+ key={entry.id}
215
+ className="bg-[var(--color-bg-tertiary)] rounded px-2 py-1.5 text-[11px] font-mono"
216
+ >
217
+ <div className="flex items-center gap-2 text-[var(--color-text-tertiary)]">
218
+ <span className="text-[var(--color-text-accent)]">{entry.eventType}</span>
219
+ <span className="text-[10px]">
220
+ {Object.keys(entry.filter).length > 0
221
+ ? `filter: ${JSON.stringify(entry.filter)}`
222
+ : t("feed.noFilter")}
223
+ </span>
224
+ <span className="text-[10px] ml-auto">
225
+ {new Date(entry.timestamp).toLocaleTimeString()}
226
+ </span>
227
+ </div>
228
+ <pre className="text-cyan-300 mt-0.5 whitespace-pre-wrap break-all">
229
+ {JSON.stringify(entry.payload, null, 0)}
230
+ </pre>
231
+ </div>
232
+ ))}
233
+ </div>
234
+ </div>
235
+ )}
221
236
 
222
- {/* Feed Posts */}
223
- {posts.length === 0 ? (
224
- <div className="flex items-center justify-center py-8 text-[var(--color-text-placeholder)] text-xs">
225
- {t("feed.noPostsYet")}
226
- </div>
227
- ) : (
228
- <div className="space-y-2">
229
- {posts.slice().reverse().map((post) => (
230
- <div key={post.id} className="bg-[var(--color-bg-secondary)] rounded-lg p-3">
231
- <div className="flex items-center gap-2 mb-1">
232
- <span className="text-xs font-medium text-[var(--color-text-primary)]">{post.author}</span>
233
- <span className={`px-1.5 py-0.5 rounded text-[10px] ${CATEGORY_COLORS[post.category]}`}>
234
- {post.category}
235
- </span>
236
- <span className="text-[10px] text-[var(--color-text-placeholder)] ml-auto">
237
- {new Date(post.timestamp).toLocaleTimeString()}
238
- </span>
239
- </div>
240
- <div className="text-xs text-[var(--color-text-secondary)]">{post.content}</div>
241
- </div>
242
- ))}
243
- </div>
244
- )}
245
- </div>
246
- </div>
247
- );
237
+ {/* Feed Posts */}
238
+ {posts.length === 0 ? (
239
+ <div className="flex items-center justify-center py-8 text-[var(--color-text-placeholder)] text-xs">
240
+ {t("feed.noPostsYet")}
241
+ </div>
242
+ ) : (
243
+ <div className="space-y-2">
244
+ {posts
245
+ .slice()
246
+ .reverse()
247
+ .map((post) => (
248
+ <div key={post.id} className="bg-[var(--color-bg-secondary)] rounded-lg p-3">
249
+ <div className="flex items-center gap-2 mb-1">
250
+ <span className="text-xs font-medium text-[var(--color-text-primary)]">
251
+ {post.author}
252
+ </span>
253
+ <span
254
+ className={`px-1.5 py-0.5 rounded text-[10px] ${CATEGORY_COLORS[post.category]}`}
255
+ >
256
+ {post.category}
257
+ </span>
258
+ <span className="text-[10px] text-[var(--color-text-placeholder)] ml-auto">
259
+ {new Date(post.timestamp).toLocaleTimeString()}
260
+ </span>
261
+ </div>
262
+ <div className="text-xs text-[var(--color-text-secondary)]">{post.content}</div>
263
+ </div>
264
+ ))}
265
+ </div>
266
+ )}
267
+ </div>
268
+ </div>
269
+ );
248
270
  }