@dyyz1993/create-agent 2.0.0 → 2.0.1

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 (141) hide show
  1. package/package.json +1 -1
  2. package/src/lib/copy.ts +23 -1
  3. package/templates/agent/eslint.config.mjs +15 -2
  4. package/templates/agent/package.json +10 -2
  5. package/templates/agent/src/__tests__/hybrid-mode.test.ts +126 -0
  6. package/templates/agent/src/__tests__/server-config-security.test.ts +55 -0
  7. package/templates/agent/src/bun/index.ts +96 -41
  8. package/templates/agent/src/bun/three.d.ts +1 -0
  9. package/templates/agent/src/gateway/__tests__/ws-handler-token.test.ts +118 -0
  10. package/templates/agent/src/gateway/http-routes.ts +1 -1
  11. package/templates/agent/src/gateway/ws-handler.ts +84 -56
  12. package/templates/agent/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  13. package/templates/agent/src/mainview/__tests__/setup.ts +32 -29
  14. package/templates/agent/src/mainview/__tests__/theme-variables.test.ts +36 -0
  15. package/templates/agent/src/mainview/components/common/ErrorBoundary.tsx +1 -58
  16. package/templates/agent/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  17. package/templates/agent/src/mainview/components/common/ThemeToggle.tsx +1 -0
  18. package/templates/agent/src/mainview/components/feed/FeedPanel.tsx +232 -210
  19. package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +149 -122
  20. package/templates/agent/src/mainview/lib/api-client.ts +182 -172
  21. package/templates/agent/src/server-config.ts +34 -32
  22. package/templates/agent/src/server.ts +43 -54
  23. package/templates/agent/src/shared/handlers/__tests__/chat-concurrency.test.ts +127 -0
  24. package/templates/agent/src/shared/handlers/__tests__/handler-isolation.test.ts +244 -0
  25. package/templates/agent/src/shared/handlers/bash.ts +65 -65
  26. package/templates/agent/src/shared/handlers/chat.ts +189 -155
  27. package/templates/agent/src/shared/handlers/debug.ts +8 -0
  28. package/templates/agent/src/shared/handlers/feed.ts +31 -32
  29. package/templates/agent/src/shared/handlers/index.ts +1 -0
  30. package/templates/agent/src/shared/handlers/rules.ts +31 -31
  31. package/templates/agent/src/shared/handlers/todo.ts +31 -31
  32. package/templates/agent/src/shared/http-routes.ts +250 -0
  33. package/templates/agent/src/shared/lib/__tests__/web-server.test.ts +190 -0
  34. package/templates/agent/src/shared/lib/logger.ts +117 -95
  35. package/templates/agent/src/shared/lib/web-server.ts +128 -0
  36. package/templates/agent/src/shared/modules/debug.ts +8 -0
  37. package/templates/agent/src/shared/rpc-schema.ts +16 -3
  38. package/templates/agent/test-upload.txt +0 -0
  39. package/templates/agent/tsconfig.ipc.json +10 -0
  40. package/templates/agent/tsconfig.json +10 -2
  41. package/templates/chat/eslint.config.mjs +15 -2
  42. package/templates/chat/package.json +10 -2
  43. package/templates/chat/src/__tests__/hybrid-mode.test.ts +125 -0
  44. package/templates/chat/src/__tests__/server-config-security.test.ts +55 -0
  45. package/templates/chat/src/__tests__/server-config.test.ts +46 -44
  46. package/templates/chat/src/bun/index.ts +94 -44
  47. package/templates/chat/src/bun/three.d.ts +1 -0
  48. package/templates/chat/src/gateway/http-routes.ts +1 -1
  49. package/templates/chat/src/gateway/ws-handler.ts +84 -56
  50. package/templates/chat/src/mainview/App.tsx +6 -0
  51. package/templates/chat/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  52. package/templates/chat/src/mainview/__tests__/theme-variables.test.ts +36 -0
  53. package/templates/chat/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  54. package/templates/chat/src/mainview/components/common/ThemeToggle.tsx +1 -0
  55. package/templates/chat/src/mainview/lib/api-client.ts +183 -156
  56. package/templates/chat/src/mainview/lib/rpc-cache.ts +84 -0
  57. package/templates/chat/src/mainview/main.tsx +4 -3
  58. package/templates/chat/src/server-config.ts +33 -31
  59. package/templates/chat/src/server.ts +43 -60
  60. package/templates/chat/src/shared/handlers/chat.ts +196 -162
  61. package/templates/chat/src/shared/handlers/debug.ts +8 -0
  62. package/templates/chat/src/shared/handlers/index.ts +1 -0
  63. package/templates/chat/src/shared/http-routes.ts +250 -0
  64. package/templates/chat/src/shared/lib/__tests__/logger.test.ts +92 -0
  65. package/templates/chat/src/shared/lib/logger.ts +108 -95
  66. package/templates/chat/src/shared/lib/web-server.ts +128 -0
  67. package/templates/chat/src/shared/modules/debug.ts +8 -0
  68. package/templates/chat/src/shared/rpc-schema.ts +3 -2
  69. package/templates/chat/test-upload.txt +0 -0
  70. package/templates/chat/tsconfig.ipc.json +10 -0
  71. package/templates/general/eslint.config.mjs +15 -2
  72. package/templates/general/package.json +10 -2
  73. package/templates/general/src/__tests__/hybrid-mode.test.ts +125 -0
  74. package/templates/general/src/__tests__/server-config-security.test.ts +55 -0
  75. package/templates/general/src/__tests__/server-config.test.ts +46 -44
  76. package/templates/general/src/bun/index.ts +94 -44
  77. package/templates/general/src/bun/three.d.ts +1 -0
  78. package/templates/general/src/gateway/http-routes.ts +1 -1
  79. package/templates/general/src/gateway/ws-handler.ts +84 -56
  80. package/templates/general/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  81. package/templates/general/src/mainview/__tests__/theme-variables.test.ts +36 -0
  82. package/templates/general/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  83. package/templates/general/src/mainview/components/common/ThemeToggle.tsx +1 -0
  84. package/templates/general/src/mainview/components/feed/FeedPanel.tsx +241 -219
  85. package/templates/general/src/mainview/components/layout/AppLayout.tsx +6 -0
  86. package/templates/general/src/mainview/lib/api-client.ts +183 -156
  87. package/templates/general/src/mainview/lib/rpc-cache.ts +84 -0
  88. package/templates/general/src/mainview/main.tsx +4 -3
  89. package/templates/general/src/server-config.ts +33 -31
  90. package/templates/general/src/server.ts +43 -60
  91. package/templates/general/src/shared/handlers/__tests__/chat.test.ts +165 -160
  92. package/templates/general/src/shared/handlers/chat.ts +189 -155
  93. package/templates/general/src/shared/handlers/debug.ts +8 -0
  94. package/templates/general/src/shared/handlers/feed.ts +38 -40
  95. package/templates/general/src/shared/handlers/index.ts +1 -0
  96. package/templates/general/src/shared/http-routes.ts +250 -0
  97. package/templates/general/src/shared/lib/__tests__/logger.test.ts +92 -0
  98. package/templates/general/src/shared/lib/logger.ts +117 -95
  99. package/templates/general/src/shared/lib/web-server.ts +128 -0
  100. package/templates/general/src/shared/modules/debug.ts +8 -0
  101. package/templates/general/src/shared/rpc-schema.ts +12 -2
  102. package/templates/general/test-upload.txt +0 -0
  103. package/templates/general/tsconfig.ipc.json +10 -0
  104. package/templates/shared/__tests__/cors-security.test.ts +165 -0
  105. package/templates/shared/__tests__/file-path-security.test.ts +260 -0
  106. package/templates/shared/__tests__/http-routes.test.ts +174 -0
  107. package/templates/shared/__tests__/logger.test.ts +186 -0
  108. package/templates/shared/components/ErrorBoundary.tsx +58 -0
  109. package/templates/shared/env.d.ts +11 -0
  110. package/templates/shared/http-routes.ts +230 -0
  111. package/templates/shared/logger.ts +129 -0
  112. package/templates/shared/test-upload.txt +0 -0
  113. package/templates/shared/vite-base.config.ts +92 -0
  114. package/templates/shared/vitest-base.config.ts +42 -0
  115. package/templates/agent/eslint-plugin-rpc/index.js +0 -50
  116. package/templates/agent/eslint-plugin-rpc/package.json +0 -6
  117. package/templates/agent/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  118. package/templates/agent/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  119. package/templates/agent/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  120. package/templates/agent/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  121. package/templates/agent/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  122. package/templates/agent/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  123. package/templates/agent/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  124. package/templates/chat/eslint-plugin-rpc/index.js +0 -50
  125. package/templates/chat/eslint-plugin-rpc/package.json +0 -6
  126. package/templates/chat/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  127. package/templates/chat/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  128. package/templates/chat/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  129. package/templates/chat/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  130. package/templates/chat/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  131. package/templates/chat/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  132. package/templates/chat/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  133. package/templates/general/eslint-plugin-rpc/index.js +0 -50
  134. package/templates/general/eslint-plugin-rpc/package.json +0 -6
  135. package/templates/general/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  136. package/templates/general/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  137. package/templates/general/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  138. package/templates/general/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  139. package/templates/general/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  140. package/templates/general/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  141. package/templates/general/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
@@ -5,235 +5,257 @@ 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
- const entries = useEventStreamStore((s) => s.entries);
41
- const activeEventType = useEventStreamStore((s) => s.activeEventType);
42
- const activeFilter = useEventStreamStore((s) => s.activeFilter);
43
- const subscribed = useEventStreamStore((s) => s.subscribed);
44
- const setActiveEventType = useEventStreamStore((s) => s.setActiveEventType);
45
- const setActiveFilter = useEventStreamStore((s) => s.setActiveFilter);
46
- const handleSubscribe = useEventStreamStore((s) => s.handleSubscribe);
47
- const handleUnsubscribe = useEventStreamStore((s) => s.handleUnsubscribe);
40
+ const entries = useEventStreamStore((s) => s.entries);
41
+ const activeEventType = useEventStreamStore((s) => s.activeEventType);
42
+ const activeFilter = useEventStreamStore((s) => s.activeFilter);
43
+ const subscribed = useEventStreamStore((s) => s.subscribed);
44
+ const setActiveEventType = useEventStreamStore((s) => s.setActiveEventType);
45
+ const setActiveFilter = useEventStreamStore((s) => s.setActiveFilter);
46
+ const handleSubscribe = useEventStreamStore((s) => s.handleSubscribe);
47
+ const handleUnsubscribe = useEventStreamStore((s) => s.handleUnsubscribe);
48
48
 
49
- useEffect(() => {
50
- if (!ready) return;
49
+ useEffect(() => {
50
+ if (!ready) return;
51
51
 
52
- let subId: string | null = null;
52
+ let subId: string | null = null;
53
53
 
54
- const setup = async () => {
55
- subId = await apiClient.subscribe("feed.update", (payload) => {
56
- addPost(payload as typeof posts[number]);
57
- }, {});
58
- addLog("Subscribed to feed.update (no filter)");
54
+ const setup = async () => {
55
+ subId = await apiClient.subscribe(
56
+ "feed.update",
57
+ (payload) => {
58
+ addPost(payload as (typeof posts)[number]);
59
+ },
60
+ {}
61
+ );
62
+ addLog("Subscribed to feed.update (no filter)");
59
63
 
60
- await loadPosts();
61
- };
62
- setup();
64
+ await loadPosts();
65
+ };
66
+ setup();
63
67
 
64
- return () => {
65
- if (subId) apiClient.unsubscribe(subId);
66
- };
67
- }, [ready, addLog, loadPosts, addPost]);
68
+ return () => {
69
+ if (subId) apiClient.unsubscribe(subId);
70
+ };
71
+ }, [ready, addLog, loadPosts, addPost]);
68
72
 
69
- return (
70
- <div className="flex-1 flex flex-col overflow-hidden">
71
- <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">
72
- <h2 className="text-sm font-semibold flex items-center gap-1.5">
73
- <Rss className="w-4 h-4 text-[var(--color-text-accent)]" />
74
- {t("feed.title")}
75
- {posts.length > 0 && (
76
- <span className="ml-1 px-2 py-0.5 bg-[var(--color-accent)]/30 text-[var(--color-text-accent)] rounded text-[10px]">
77
- {posts.length}
78
- </span>
79
- )}
80
- </h2>
81
- </div>
73
+ return (
74
+ <div className="flex-1 flex flex-col overflow-hidden">
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>
82
86
 
83
- <div className="flex-1 overflow-y-auto p-4 space-y-4">
84
- <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
85
- <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">{t("feed.newPost")}</div>
86
- <div className="flex gap-2">
87
- <input
88
- type="text"
89
- value={author}
90
- onChange={(e) => setAuthor(e.target.value)}
91
- placeholder={t("feed.authorPlaceholder")}
92
- 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"
93
- />
94
- <select
95
- value={category}
96
- onChange={(e) => setCategory(e.target.value as FeedCategory)}
97
- 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)]"
98
- >
99
- {CATEGORIES.map((c) => (
100
- <option key={c} value={c}>{c}</option>
101
- ))}
102
- </select>
103
- </div>
104
- <div className="flex gap-2">
105
- <input
106
- type="text"
107
- value={content}
108
- onChange={(e) => setContent(e.target.value)}
109
- onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && createPost()}
110
- placeholder={t("feed.postPlaceholder")}
111
- 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"
112
- />
113
- <button
114
- onClick={createPost}
115
- disabled={loading || !content.trim()}
116
- 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"
117
- >
118
- <Send className="w-3 h-3" />
119
- {t("feed.post")}
120
- </button>
121
- </div>
122
- </div>
87
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
88
+ <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
89
+ <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">
90
+ {t("feed.newPost")}
91
+ </div>
92
+ <div className="flex gap-2">
93
+ <input
94
+ type="text"
95
+ value={author}
96
+ onChange={(e) => setAuthor(e.target.value)}
97
+ placeholder={t("feed.authorPlaceholder")}
98
+ 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"
99
+ />
100
+ <select
101
+ value={category}
102
+ onChange={(e) => setCategory(e.target.value as FeedCategory)}
103
+ 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)]"
104
+ >
105
+ {CATEGORIES.map((c) => (
106
+ <option key={c} value={c}>
107
+ {c}
108
+ </option>
109
+ ))}
110
+ </select>
111
+ </div>
112
+ <div className="flex gap-2">
113
+ <input
114
+ type="text"
115
+ value={content}
116
+ onChange={(e) => setContent(e.target.value)}
117
+ onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && createPost()}
118
+ placeholder={t("feed.postPlaceholder")}
119
+ 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"
120
+ />
121
+ <button
122
+ onClick={createPost}
123
+ disabled={loading || !content.trim()}
124
+ 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"
125
+ >
126
+ <Send className="w-3 h-3" />
127
+ {t("feed.post")}
128
+ </button>
129
+ </div>
130
+ </div>
123
131
 
124
- <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
125
- <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">{t("feed.subscribeWithFilter")}</div>
126
- <div className="flex gap-2 items-center">
127
- <select
128
- value={activeEventType}
129
- onChange={(e) => {
130
- setActiveEventType(e.target.value as SubEventType);
131
- setActiveFilter("");
132
- }}
133
- 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)]"
134
- >
135
- {EVENT_TYPES.map((et) => (
136
- <option key={et} value={et}>{et}</option>
137
- ))}
138
- </select>
139
- <input
140
- type="text"
141
- value={activeFilter}
142
- onChange={(e) => setActiveFilter(e.target.value)}
143
- placeholder={t("feed.filterPlaceholder")}
144
- 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"
145
- />
146
- {!subscribed ? (
147
- <button
148
- onClick={handleSubscribe}
149
- className="px-3 py-1.5 text-xs bg-green-600 hover:bg-green-700 rounded transition-colors whitespace-nowrap"
150
- >
151
- {t("feed.subscribe")}
152
- </button>
153
- ) : (
154
- <button
155
- onClick={handleUnsubscribe}
156
- className="px-3 py-1.5 text-xs bg-red-600 hover:bg-red-700 rounded transition-colors whitespace-nowrap"
157
- >
158
- {t("feed.unsubscribe")}
159
- </button>
160
- )}
161
- </div>
162
- <div className="flex gap-1 flex-wrap">
163
- {FILTER_PRESETS[activeEventType].map((preset) => (
164
- <button
165
- key={preset}
166
- onClick={() => setActiveFilter(preset)}
167
- className={`px-2 py-0.5 text-[10px] rounded border transition-colors ${
168
- activeFilter === preset
169
- ? "border-[var(--color-accent)] bg-[var(--color-accent)]/30 text-[var(--color-text-accent)]"
170
- : "border-[var(--color-border-secondary)] text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]"
171
- }`}
172
- >
173
- {preset || t("feed.noFilter")}
174
- </button>
175
- ))}
176
- </div>
177
- </div>
132
+ <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
133
+ <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">
134
+ {t("feed.subscribeWithFilter")}
135
+ </div>
136
+ <div className="flex gap-2 items-center">
137
+ <select
138
+ value={activeEventType}
139
+ onChange={(e) => {
140
+ setActiveEventType(e.target.value as SubEventType);
141
+ setActiveFilter("");
142
+ }}
143
+ 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)]"
144
+ >
145
+ {EVENT_TYPES.map((et) => (
146
+ <option key={et} value={et}>
147
+ {et}
148
+ </option>
149
+ ))}
150
+ </select>
151
+ <input
152
+ type="text"
153
+ value={activeFilter}
154
+ onChange={(e) => setActiveFilter(e.target.value)}
155
+ placeholder={t("feed.filterPlaceholder")}
156
+ 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"
157
+ />
158
+ {!subscribed ? (
159
+ <button
160
+ onClick={handleSubscribe}
161
+ className="px-3 py-1.5 text-xs bg-green-600 hover:bg-green-700 rounded transition-colors whitespace-nowrap"
162
+ >
163
+ {t("feed.subscribe")}
164
+ </button>
165
+ ) : (
166
+ <button
167
+ onClick={handleUnsubscribe}
168
+ className="px-3 py-1.5 text-xs bg-red-600 hover:bg-red-700 rounded transition-colors whitespace-nowrap"
169
+ >
170
+ {t("feed.unsubscribe")}
171
+ </button>
172
+ )}
173
+ </div>
174
+ <div className="flex gap-1 flex-wrap">
175
+ {FILTER_PRESETS[activeEventType].map((preset) => (
176
+ <button
177
+ key={preset}
178
+ onClick={() => setActiveFilter(preset)}
179
+ className={`px-2 py-0.5 text-[10px] rounded border transition-colors ${
180
+ activeFilter === preset
181
+ ? "border-[var(--color-accent)] bg-[var(--color-accent)]/30 text-[var(--color-text-accent)]"
182
+ : "border-[var(--color-border-secondary)] text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]"
183
+ }`}
184
+ >
185
+ {preset || t("feed.noFilter")}
186
+ </button>
187
+ ))}
188
+ </div>
189
+ </div>
178
190
 
179
- {entries.length > 0 && (
180
- <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
181
- <div className="flex items-center justify-between">
182
- <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">
183
- {t("feed.eventStream")}
184
- </div>
185
- {subscribed && (
186
- <span className="px-1.5 py-0.5 bg-green-600/30 text-green-400 rounded text-[10px] animate-pulse">
187
- {t("feed.live")}
188
- </span>
189
- )}
190
- </div>
191
- <div className="max-h-48 overflow-y-auto space-y-1">
192
- {entries.slice(-20).map((entry) => (
193
- <div key={entry.id} className="bg-[var(--color-bg-tertiary)] rounded px-2 py-1.5 text-[11px] font-mono">
194
- <div className="flex items-center gap-2 text-[var(--color-text-tertiary)]">
195
- <span className="text-[var(--color-text-accent)]">{entry.eventType}</span>
196
- <span className="text-[10px]">
197
- {Object.keys(entry.filter).length > 0
198
- ? `filter: ${JSON.stringify(entry.filter)}`
199
- : t("feed.noFilter")}
200
- </span>
201
- <span className="text-[10px] ml-auto">
202
- {new Date(entry.timestamp).toLocaleTimeString()}
203
- </span>
204
- </div>
205
- <pre className="text-cyan-300 mt-0.5 whitespace-pre-wrap break-all">
206
- {JSON.stringify(entry.payload, null, 0)}
207
- </pre>
208
- </div>
209
- ))}
210
- </div>
211
- </div>
212
- )}
191
+ {entries.length > 0 && (
192
+ <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
193
+ <div className="flex items-center justify-between">
194
+ <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">
195
+ {t("feed.eventStream")}
196
+ </div>
197
+ {subscribed && (
198
+ <span className="px-1.5 py-0.5 bg-green-600/30 text-green-400 rounded text-[10px] animate-pulse">
199
+ {t("feed.live")}
200
+ </span>
201
+ )}
202
+ </div>
203
+ <div className="max-h-48 overflow-y-auto space-y-1">
204
+ {entries.slice(-20).map((entry) => (
205
+ <div
206
+ key={entry.id}
207
+ className="bg-[var(--color-bg-tertiary)] rounded px-2 py-1.5 text-[11px] font-mono"
208
+ >
209
+ <div className="flex items-center gap-2 text-[var(--color-text-tertiary)]">
210
+ <span className="text-[var(--color-text-accent)]">{entry.eventType}</span>
211
+ <span className="text-[10px]">
212
+ {Object.keys(entry.filter).length > 0
213
+ ? `filter: ${JSON.stringify(entry.filter)}`
214
+ : t("feed.noFilter")}
215
+ </span>
216
+ <span className="text-[10px] ml-auto">
217
+ {new Date(entry.timestamp).toLocaleTimeString()}
218
+ </span>
219
+ </div>
220
+ <pre className="text-cyan-300 mt-0.5 whitespace-pre-wrap break-all">
221
+ {JSON.stringify(entry.payload, null, 0)}
222
+ </pre>
223
+ </div>
224
+ ))}
225
+ </div>
226
+ </div>
227
+ )}
213
228
 
214
- {posts.length === 0 ? (
215
- <div className="flex items-center justify-center py-8 text-[var(--color-text-placeholder)] text-xs">
216
- {t("feed.noPostsYet")}
217
- </div>
218
- ) : (
219
- <div className="space-y-2">
220
- {posts.slice().reverse().map((post) => (
221
- <div key={post.id} className="bg-[var(--color-bg-secondary)] rounded-lg p-3">
222
- <div className="flex items-center gap-2 mb-1">
223
- <span className="text-xs font-medium text-[var(--color-text-primary)]">{post.author}</span>
224
- <span className={`px-1.5 py-0.5 rounded text-[10px] ${CATEGORY_COLORS[post.category]}`}>
225
- {post.category}
226
- </span>
227
- <span className="text-[10px] text-[var(--color-text-placeholder)] ml-auto">
228
- {new Date(post.timestamp).toLocaleTimeString()}
229
- </span>
230
- </div>
231
- <div className="text-xs text-[var(--color-text-secondary)]">{post.content}</div>
232
- </div>
233
- ))}
234
- </div>
235
- )}
236
- </div>
237
- </div>
238
- );
229
+ {posts.length === 0 ? (
230
+ <div className="flex items-center justify-center py-8 text-[var(--color-text-placeholder)] text-xs">
231
+ {t("feed.noPostsYet")}
232
+ </div>
233
+ ) : (
234
+ <div className="space-y-2">
235
+ {posts
236
+ .slice()
237
+ .reverse()
238
+ .map((post) => (
239
+ <div key={post.id} className="bg-[var(--color-bg-secondary)] rounded-lg p-3">
240
+ <div className="flex items-center gap-2 mb-1">
241
+ <span className="text-xs font-medium text-[var(--color-text-primary)]">
242
+ {post.author}
243
+ </span>
244
+ <span
245
+ className={`px-1.5 py-0.5 rounded text-[10px] ${CATEGORY_COLORS[post.category]}`}
246
+ >
247
+ {post.category}
248
+ </span>
249
+ <span className="text-[10px] text-[var(--color-text-placeholder)] ml-auto">
250
+ {new Date(post.timestamp).toLocaleTimeString()}
251
+ </span>
252
+ </div>
253
+ <div className="text-xs text-[var(--color-text-secondary)]">{post.content}</div>
254
+ </div>
255
+ ))}
256
+ </div>
257
+ )}
258
+ </div>
259
+ </div>
260
+ );
239
261
  }