@atercates/claude-deck 0.2.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 (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/app/api/claude/hidden/route.ts +66 -0
  4. package/app/api/claude/projects/[name]/sessions/route.ts +71 -0
  5. package/app/api/claude/projects/route.ts +44 -0
  6. package/app/api/code-search/available/route.ts +12 -0
  7. package/app/api/code-search/route.ts +47 -0
  8. package/app/api/dev-servers/[id]/logs/route.ts +23 -0
  9. package/app/api/dev-servers/[id]/restart/route.ts +20 -0
  10. package/app/api/dev-servers/[id]/route.ts +51 -0
  11. package/app/api/dev-servers/[id]/stop/route.ts +20 -0
  12. package/app/api/dev-servers/detect/route.ts +39 -0
  13. package/app/api/dev-servers/route.ts +48 -0
  14. package/app/api/exec/route.ts +60 -0
  15. package/app/api/files/content/route.ts +76 -0
  16. package/app/api/files/route.ts +37 -0
  17. package/app/api/files/upload-temp/route.ts +41 -0
  18. package/app/api/git/check/route.ts +54 -0
  19. package/app/api/git/clone/route.ts +99 -0
  20. package/app/api/git/commit/route.ts +75 -0
  21. package/app/api/git/discard/route.ts +38 -0
  22. package/app/api/git/file-content/route.ts +64 -0
  23. package/app/api/git/history/[hash]/diff/route.ts +38 -0
  24. package/app/api/git/history/[hash]/route.ts +34 -0
  25. package/app/api/git/history/route.ts +27 -0
  26. package/app/api/git/multi-status/route.ts +46 -0
  27. package/app/api/git/pr/route.ts +164 -0
  28. package/app/api/git/push/route.ts +64 -0
  29. package/app/api/git/stage/route.ts +40 -0
  30. package/app/api/git/status/route.ts +51 -0
  31. package/app/api/git/unstage/route.ts +46 -0
  32. package/app/api/groups/[...path]/route.ts +136 -0
  33. package/app/api/groups/route.ts +93 -0
  34. package/app/api/orchestrate/spawn/route.ts +45 -0
  35. package/app/api/orchestrate/workers/[id]/route.ts +89 -0
  36. package/app/api/orchestrate/workers/route.ts +31 -0
  37. package/app/api/projects/[id]/detect/route.ts +27 -0
  38. package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +66 -0
  39. package/app/api/projects/[id]/dev-servers/route.ts +51 -0
  40. package/app/api/projects/[id]/repositories/[repoId]/route.ts +67 -0
  41. package/app/api/projects/[id]/repositories/route.ts +74 -0
  42. package/app/api/projects/[id]/route.ts +108 -0
  43. package/app/api/projects/detect/route.ts +33 -0
  44. package/app/api/projects/route.ts +59 -0
  45. package/app/api/sessions/[id]/claude-session/route.ts +42 -0
  46. package/app/api/sessions/[id]/fork/route.ts +74 -0
  47. package/app/api/sessions/[id]/mcp-config/route.ts +34 -0
  48. package/app/api/sessions/[id]/messages/route.ts +60 -0
  49. package/app/api/sessions/[id]/pr/route.ts +188 -0
  50. package/app/api/sessions/[id]/preview/route.ts +42 -0
  51. package/app/api/sessions/[id]/route.ts +229 -0
  52. package/app/api/sessions/[id]/send-keys/route.ts +119 -0
  53. package/app/api/sessions/[id]/summarize/route.ts +331 -0
  54. package/app/api/sessions/init-script/route.ts +84 -0
  55. package/app/api/sessions/route.ts +209 -0
  56. package/app/api/sessions/status/route.ts +237 -0
  57. package/app/api/system/route.ts +9 -0
  58. package/app/api/tmux/kill-all/route.ts +57 -0
  59. package/app/api/tmux/rename/route.ts +30 -0
  60. package/app/globals.css +174 -0
  61. package/app/icon.svg +11 -0
  62. package/app/layout.tsx +122 -0
  63. package/app/page.tsx +629 -0
  64. package/components/ChatMessage.tsx +65 -0
  65. package/components/ChatView.tsx +276 -0
  66. package/components/ClaudeProjects/ClaudeProjectCard.tsx +195 -0
  67. package/components/ClaudeProjects/ClaudeProjectsSection.tsx +89 -0
  68. package/components/ClaudeProjects/ClaudeSessionCard.tsx +100 -0
  69. package/components/ClaudeProjects/index.ts +1 -0
  70. package/components/CodeSearch/CodeSearchResults.tsx +177 -0
  71. package/components/ConductorPanel.tsx +256 -0
  72. package/components/DevServers/DevServerCard.tsx +311 -0
  73. package/components/DevServers/DevServersSection.tsx +91 -0
  74. package/components/DevServers/ServerLogsModal.tsx +151 -0
  75. package/components/DevServers/StartServerDialog.tsx +359 -0
  76. package/components/DevServers/index.ts +4 -0
  77. package/components/DiffViewer/DiffModal.tsx +151 -0
  78. package/components/DiffViewer/UnifiedDiff.tsx +185 -0
  79. package/components/DiffViewer/index.tsx +2 -0
  80. package/components/DirectoryPicker.tsx +355 -0
  81. package/components/FileExplorer/FileEditor.tsx +276 -0
  82. package/components/FileExplorer/FileTabs.tsx +118 -0
  83. package/components/FileExplorer/FileTree.tsx +214 -0
  84. package/components/FileExplorer/HtmlRenderer.tsx +16 -0
  85. package/components/FileExplorer/MarkdownRenderer.tsx +18 -0
  86. package/components/FileExplorer/index.tsx +520 -0
  87. package/components/FilePicker.tsx +339 -0
  88. package/components/FolderPicker.tsx +201 -0
  89. package/components/GitDrawer/FileEditDialog.tsx +400 -0
  90. package/components/GitDrawer/index.tsx +464 -0
  91. package/components/GitPanel/CommitForm.tsx +205 -0
  92. package/components/GitPanel/CommitHistory.tsx +174 -0
  93. package/components/GitPanel/CommitItem.tsx +196 -0
  94. package/components/GitPanel/FileChanges.tsx +414 -0
  95. package/components/GitPanel/GitPanelTabs.tsx +39 -0
  96. package/components/GitPanel/index.tsx +817 -0
  97. package/components/MessageInput.tsx +82 -0
  98. package/components/NewClaudeSessionDialog.tsx +166 -0
  99. package/components/NewSessionDialog/AdvancedSettings.tsx +78 -0
  100. package/components/NewSessionDialog/AgentSelector.tsx +37 -0
  101. package/components/NewSessionDialog/CreatingOverlay.tsx +94 -0
  102. package/components/NewSessionDialog/NewSessionDialog.types.ts +136 -0
  103. package/components/NewSessionDialog/ProjectSelector.tsx +146 -0
  104. package/components/NewSessionDialog/WorkingDirectoryInput.tsx +55 -0
  105. package/components/NewSessionDialog/WorktreeSection.tsx +92 -0
  106. package/components/NewSessionDialog/hooks/useNewSessionForm.ts +370 -0
  107. package/components/NewSessionDialog/index.tsx +106 -0
  108. package/components/NotificationSettings.tsx +127 -0
  109. package/components/PRCreationModal.tsx +272 -0
  110. package/components/Pane/DesktopTabBar.tsx +353 -0
  111. package/components/Pane/MobileTabBar.tsx +210 -0
  112. package/components/Pane/OpenInVSCode.tsx +69 -0
  113. package/components/Pane/PaneSkeletons.tsx +57 -0
  114. package/components/Pane/index.tsx +558 -0
  115. package/components/PaneLayout.tsx +60 -0
  116. package/components/Projects/DevServersSection.tsx +140 -0
  117. package/components/Projects/DirectoryField.tsx +92 -0
  118. package/components/Projects/NewProjectDialog.tsx +188 -0
  119. package/components/Projects/NewProjectDialog.types.ts +46 -0
  120. package/components/Projects/ProjectCard.tsx +276 -0
  121. package/components/Projects/ProjectSettingsDialog.tsx +811 -0
  122. package/components/Projects/hooks/useNewProjectForm.ts +249 -0
  123. package/components/Projects/index.ts +3 -0
  124. package/components/Providers.tsx +49 -0
  125. package/components/QuickSwitcher.tsx +306 -0
  126. package/components/SessionList/KillAllConfirm.tsx +46 -0
  127. package/components/SessionList/SelectionToolbar.tsx +164 -0
  128. package/components/SessionList/SessionList.types.ts +37 -0
  129. package/components/SessionList/SessionListHeader.tsx +71 -0
  130. package/components/SessionList/hooks/useSessionListMutations.ts +269 -0
  131. package/components/SessionList/index.tsx +189 -0
  132. package/components/ShellDrawer/index.tsx +106 -0
  133. package/components/SidebarFooter.tsx +55 -0
  134. package/components/Terminal/KeybarToggleButton.tsx +45 -0
  135. package/components/Terminal/ScrollToBottomButton.tsx +32 -0
  136. package/components/Terminal/SearchBar.tsx +71 -0
  137. package/components/Terminal/TerminalToolbar.tsx +551 -0
  138. package/components/Terminal/VirtualKeyboard.tsx +711 -0
  139. package/components/Terminal/constants.ts +20 -0
  140. package/components/Terminal/hooks/index.ts +5 -0
  141. package/components/Terminal/hooks/resize-handlers.ts +140 -0
  142. package/components/Terminal/hooks/terminal-init.ts +151 -0
  143. package/components/Terminal/hooks/touch-scroll.ts +155 -0
  144. package/components/Terminal/hooks/useTerminalConnection.ts +282 -0
  145. package/components/Terminal/hooks/useTerminalConnection.types.ts +39 -0
  146. package/components/Terminal/hooks/useTerminalSearch.ts +103 -0
  147. package/components/Terminal/hooks/websocket-connection.ts +274 -0
  148. package/components/Terminal/index.tsx +320 -0
  149. package/components/ThemeToggle.tsx +168 -0
  150. package/components/TmuxSessions.tsx +132 -0
  151. package/components/ToolCallDisplay.tsx +71 -0
  152. package/components/WorkerCard.tsx +245 -0
  153. package/components/a/ABadge.tsx +115 -0
  154. package/components/a/AButton.tsx +163 -0
  155. package/components/a/ADialog.tsx +93 -0
  156. package/components/a/ADropdownMenu.tsx +279 -0
  157. package/components/a/AIconButton.tsx +190 -0
  158. package/components/a/ASheet.tsx +150 -0
  159. package/components/a/ATooltip.tsx +77 -0
  160. package/components/a/index.ts +64 -0
  161. package/components/mobile/SwipeSidebar.tsx +122 -0
  162. package/components/ui/badge.tsx +41 -0
  163. package/components/ui/button.tsx +60 -0
  164. package/components/ui/context-menu.tsx +197 -0
  165. package/components/ui/dialog.tsx +143 -0
  166. package/components/ui/dropdown-menu.tsx +257 -0
  167. package/components/ui/input.tsx +21 -0
  168. package/components/ui/scroll-area.tsx +52 -0
  169. package/components/ui/select.tsx +159 -0
  170. package/components/ui/skeleton.tsx +111 -0
  171. package/components/ui/switch.tsx +31 -0
  172. package/components/ui/textarea.tsx +21 -0
  173. package/components/ui/tooltip.tsx +32 -0
  174. package/components/views/DesktopView.tsx +244 -0
  175. package/components/views/MobileView.tsx +110 -0
  176. package/components/views/types.ts +75 -0
  177. package/contexts/PaneContext.tsx +336 -0
  178. package/data/claude/index.ts +9 -0
  179. package/data/claude/keys.ts +6 -0
  180. package/data/claude/queries.ts +120 -0
  181. package/data/claude/useClaudeUpdates.ts +37 -0
  182. package/data/code-search/index.ts +2 -0
  183. package/data/code-search/keys.ts +7 -0
  184. package/data/code-search/queries.ts +61 -0
  185. package/data/dev-servers/index.ts +8 -0
  186. package/data/dev-servers/keys.ts +4 -0
  187. package/data/dev-servers/queries.ts +104 -0
  188. package/data/files/index.ts +3 -0
  189. package/data/files/keys.ts +4 -0
  190. package/data/files/queries.ts +25 -0
  191. package/data/git/keys.ts +15 -0
  192. package/data/git/queries.ts +395 -0
  193. package/data/groups/index.ts +1 -0
  194. package/data/groups/mutations.ts +95 -0
  195. package/data/projects/index.ts +10 -0
  196. package/data/projects/keys.ts +4 -0
  197. package/data/projects/queries.ts +193 -0
  198. package/data/repositories/index.ts +7 -0
  199. package/data/repositories/keys.ts +5 -0
  200. package/data/repositories/queries.ts +122 -0
  201. package/data/sessions/index.ts +12 -0
  202. package/data/sessions/keys.ts +8 -0
  203. package/data/sessions/queries.ts +218 -0
  204. package/data/statuses/index.ts +1 -0
  205. package/data/statuses/queries.ts +69 -0
  206. package/hooks/useCopyToClipboard.ts +48 -0
  207. package/hooks/useDevServersManager.ts +73 -0
  208. package/hooks/useDirectoryBrowser.ts +90 -0
  209. package/hooks/useDrawerAnimation.ts +27 -0
  210. package/hooks/useFileDrop.ts +87 -0
  211. package/hooks/useFileEditor.ts +184 -0
  212. package/hooks/useGroups.ts +37 -0
  213. package/hooks/useHomePath.ts +34 -0
  214. package/hooks/useKeyRepeat.ts +55 -0
  215. package/hooks/useKeybarVisibility.ts +42 -0
  216. package/hooks/useNotifications.ts +257 -0
  217. package/hooks/useProjects.ts +53 -0
  218. package/hooks/useSessionStatuses.ts +30 -0
  219. package/hooks/useSessions.ts +86 -0
  220. package/hooks/useSpeechRecognition.ts +124 -0
  221. package/hooks/useViewport.ts +32 -0
  222. package/hooks/useViewportHeight.ts +50 -0
  223. package/lib/async-operations.ts +35 -0
  224. package/lib/banner.ts +81 -0
  225. package/lib/claude/jsonl-cache.ts +86 -0
  226. package/lib/claude/jsonl-reader.ts +271 -0
  227. package/lib/claude/process-manager.ts +278 -0
  228. package/lib/claude/stream-parser.ts +173 -0
  229. package/lib/claude/types.ts +154 -0
  230. package/lib/claude/watcher.ts +71 -0
  231. package/lib/client/session-registry.ts +111 -0
  232. package/lib/code-search.ts +121 -0
  233. package/lib/db/index.ts +48 -0
  234. package/lib/db/migrations.ts +45 -0
  235. package/lib/db/queries.ts +460 -0
  236. package/lib/db/schema.ts +114 -0
  237. package/lib/db/types.ts +92 -0
  238. package/lib/db.ts +2 -0
  239. package/lib/dev-servers.ts +509 -0
  240. package/lib/diff-parser.ts +221 -0
  241. package/lib/env-setup.ts +285 -0
  242. package/lib/file-upload.ts +34 -0
  243. package/lib/file-utils.ts +50 -0
  244. package/lib/files.ts +207 -0
  245. package/lib/git-history.ts +294 -0
  246. package/lib/git-status.ts +391 -0
  247. package/lib/git.ts +257 -0
  248. package/lib/mcp-config.ts +81 -0
  249. package/lib/multi-repo-git.ts +179 -0
  250. package/lib/notifications.ts +219 -0
  251. package/lib/orchestration.ts +448 -0
  252. package/lib/panes.ts +232 -0
  253. package/lib/ports.ts +97 -0
  254. package/lib/pr-generation.ts +307 -0
  255. package/lib/pr.ts +234 -0
  256. package/lib/projects.ts +578 -0
  257. package/lib/providers/registry.ts +70 -0
  258. package/lib/providers.ts +121 -0
  259. package/lib/query-client.ts +14 -0
  260. package/lib/rangeSelectionUtils.ts +65 -0
  261. package/lib/status-detector.ts +375 -0
  262. package/lib/terminal-themes.ts +265 -0
  263. package/lib/theme-config.ts +327 -0
  264. package/lib/utils.ts +6 -0
  265. package/lib/worktrees.ts +262 -0
  266. package/mcp/orchestration-server.ts +438 -0
  267. package/package.json +139 -0
  268. package/postcss.config.mjs +7 -0
  269. package/public/icon.svg +10 -0
  270. package/public/icons/icon-128x128.png +0 -0
  271. package/public/icons/icon-144x144.png +0 -0
  272. package/public/icons/icon-152x152.png +0 -0
  273. package/public/icons/icon-192x192.png +0 -0
  274. package/public/icons/icon-384x384.png +0 -0
  275. package/public/icons/icon-512x512.png +0 -0
  276. package/public/icons/icon-72x72.png +0 -0
  277. package/public/icons/icon-96x96.png +0 -0
  278. package/public/manifest.json +61 -0
  279. package/public/sw.js +64 -0
  280. package/scripts/agent-os +91 -0
  281. package/scripts/install.sh +48 -0
  282. package/scripts/lib/ai-clis.sh +132 -0
  283. package/scripts/lib/commands.sh +487 -0
  284. package/scripts/lib/common.sh +89 -0
  285. package/scripts/lib/prerequisites.sh +462 -0
  286. package/scripts/setup.sh +134 -0
  287. package/server.ts +155 -0
  288. package/stores/fileOpen.ts +26 -0
  289. package/stores/index.ts +1 -0
  290. package/stores/initialPrompt.ts +24 -0
  291. package/stores/sessionSelection.ts +48 -0
  292. package/styles/themes.css +603 -0
  293. package/tsconfig.json +33 -0
@@ -0,0 +1,276 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { ChatMessage } from "./ChatMessage";
5
+ import { ToolCallDisplay } from "./ToolCallDisplay";
6
+ import { MessageInput } from "./MessageInput";
7
+ import { ScrollArea } from "./ui/scroll-area";
8
+ import type { ClientEvent } from "@/lib/claude/types";
9
+
10
+ interface ChatViewProps {
11
+ sessionId: string;
12
+ }
13
+
14
+ interface DisplayMessage {
15
+ id: string;
16
+ role: "user" | "assistant";
17
+ content: string;
18
+ timestamp: string;
19
+ isStreaming?: boolean;
20
+ toolCalls?: Array<{
21
+ name: string;
22
+ input: Record<string, unknown>;
23
+ output?: string;
24
+ status: "pending" | "running" | "completed" | "error";
25
+ }>;
26
+ }
27
+
28
+ export function ChatView({ sessionId }: ChatViewProps) {
29
+ const [messages, setMessages] = useState<DisplayMessage[]>([]);
30
+ const [currentText, setCurrentText] = useState("");
31
+ const [currentToolCalls, setCurrentToolCalls] = useState<
32
+ DisplayMessage["toolCalls"]
33
+ >([]);
34
+ const [status, setStatus] = useState<
35
+ "idle" | "running" | "waiting" | "error"
36
+ >("idle");
37
+ const wsRef = useRef<WebSocket | null>(null);
38
+ const scrollRef = useRef<HTMLDivElement>(null);
39
+
40
+ // Refs to track current streaming state (avoids closure issues)
41
+ const currentTextRef = useRef("");
42
+ const currentToolCallsRef = useRef<DisplayMessage["toolCalls"]>([]);
43
+
44
+ // Reset messages when session changes
45
+ useEffect(() => {
46
+ setMessages([]);
47
+ setCurrentText("");
48
+ setCurrentToolCalls([]);
49
+ currentTextRef.current = "";
50
+ currentToolCallsRef.current = [];
51
+ }, [sessionId]);
52
+
53
+ useEffect(() => {
54
+ let ignore = false;
55
+ let ws: WebSocket | null = null;
56
+
57
+ // Connect to Claude WebSocket
58
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
59
+ ws = new WebSocket(
60
+ `${protocol}//${window.location.host}/ws/claude/${sessionId}`
61
+ );
62
+
63
+ ws.onopen = () => {
64
+ if (ignore) {
65
+ ws?.close();
66
+ return;
67
+ }
68
+ wsRef.current = ws;
69
+ };
70
+
71
+ ws.onmessage = (event) => {
72
+ if (ignore) return;
73
+ const data: ClientEvent = JSON.parse(event.data);
74
+
75
+ switch (data.type) {
76
+ case "status":
77
+ setStatus(
78
+ (data.data as { status: "idle" | "running" | "waiting" | "error" })
79
+ .status
80
+ );
81
+ break;
82
+
83
+ case "text": {
84
+ const textData = data.data as { role: string; text: string };
85
+ if (textData.role === "assistant") {
86
+ setCurrentText((prev) => {
87
+ const newText = prev + textData.text;
88
+ currentTextRef.current = newText;
89
+ return newText;
90
+ });
91
+ }
92
+ break;
93
+ }
94
+
95
+ case "tool_start": {
96
+ const toolStartData = data.data as {
97
+ toolName: string;
98
+ input: Record<string, unknown>;
99
+ };
100
+ setCurrentToolCalls((prev) => {
101
+ const newCalls = [
102
+ ...(prev || []),
103
+ {
104
+ name: toolStartData.toolName,
105
+ input: toolStartData.input,
106
+ status: "running" as const,
107
+ },
108
+ ];
109
+ currentToolCallsRef.current = newCalls;
110
+ return newCalls;
111
+ });
112
+ break;
113
+ }
114
+
115
+ case "tool_end": {
116
+ const toolEndData = data.data as {
117
+ toolName: string;
118
+ output: string;
119
+ status: string;
120
+ };
121
+ setCurrentToolCalls((prev) => {
122
+ const newCalls = prev?.map((tc) =>
123
+ tc.name === toolEndData.toolName && tc.status === "running"
124
+ ? {
125
+ ...tc,
126
+ output: toolEndData.output,
127
+ status: toolEndData.status as "completed" | "error",
128
+ }
129
+ : tc
130
+ );
131
+ currentToolCallsRef.current = newCalls;
132
+ return newCalls;
133
+ });
134
+ break;
135
+ }
136
+
137
+ case "complete":
138
+ case "error": {
139
+ // Finalize current message using refs (avoids stale closure)
140
+ const finalText = currentTextRef.current;
141
+ const finalToolCalls = currentToolCallsRef.current;
142
+
143
+ if (finalText || (finalToolCalls && finalToolCalls.length > 0)) {
144
+ setMessages((prev) => [
145
+ ...prev,
146
+ {
147
+ id: `msg-${Date.now()}`,
148
+ role: "assistant",
149
+ content: finalText,
150
+ timestamp: data.timestamp,
151
+ toolCalls: finalToolCalls,
152
+ },
153
+ ]);
154
+ }
155
+ // Reset state and refs
156
+ setCurrentText("");
157
+ setCurrentToolCalls([]);
158
+ currentTextRef.current = "";
159
+ currentToolCallsRef.current = [];
160
+ break;
161
+ }
162
+ }
163
+ };
164
+
165
+ ws.onclose = () => {
166
+ if (!ignore) {
167
+ setStatus("idle");
168
+ }
169
+ };
170
+
171
+ return () => {
172
+ ignore = true;
173
+ wsRef.current = null;
174
+ if (ws && ws.readyState === WebSocket.OPEN) {
175
+ ws.close();
176
+ }
177
+ };
178
+ }, [sessionId]);
179
+
180
+ // Auto-scroll to bottom
181
+ useEffect(() => {
182
+ scrollRef.current?.scrollIntoView({ behavior: "smooth" });
183
+ }, [messages, currentText]);
184
+
185
+ const sendMessage = (text: string) => {
186
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
187
+
188
+ // Add user message to display
189
+ setMessages((prev) => [
190
+ ...prev,
191
+ {
192
+ id: `user-${Date.now()}`,
193
+ role: "user",
194
+ content: text,
195
+ timestamp: new Date().toISOString(),
196
+ },
197
+ ]);
198
+
199
+ // Send to Claude
200
+ wsRef.current.send(
201
+ JSON.stringify({
202
+ type: "prompt",
203
+ prompt: text,
204
+ options: { resume: true },
205
+ })
206
+ );
207
+ };
208
+
209
+ const cancelRequest = () => {
210
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
211
+ wsRef.current.send(JSON.stringify({ type: "cancel" }));
212
+ }
213
+ };
214
+
215
+ return (
216
+ <div className="flex h-full flex-col">
217
+ <ScrollArea className="flex-1 p-4">
218
+ <div className="space-y-4">
219
+ {messages.map((msg) => (
220
+ <div key={msg.id}>
221
+ <ChatMessage
222
+ role={msg.role}
223
+ content={msg.content}
224
+ timestamp={msg.timestamp}
225
+ />
226
+ {msg.toolCalls?.map((tc, i) => (
227
+ <ToolCallDisplay
228
+ key={`${msg.id}-tool-${i}`}
229
+ name={tc.name}
230
+ input={tc.input}
231
+ output={tc.output}
232
+ status={tc.status}
233
+ />
234
+ ))}
235
+ </div>
236
+ ))}
237
+
238
+ {/* Streaming content */}
239
+ {(currentText ||
240
+ (currentToolCalls && currentToolCalls.length > 0)) && (
241
+ <div>
242
+ {currentText && (
243
+ <ChatMessage
244
+ role="assistant"
245
+ content={currentText}
246
+ timestamp={new Date().toISOString()}
247
+ isStreaming
248
+ />
249
+ )}
250
+ {currentToolCalls?.map((tc, i) => (
251
+ <ToolCallDisplay
252
+ key={`streaming-tool-${i}`}
253
+ name={tc.name}
254
+ input={tc.input}
255
+ output={tc.output}
256
+ status={tc.status}
257
+ />
258
+ ))}
259
+ </div>
260
+ )}
261
+
262
+ <div ref={scrollRef} />
263
+ </div>
264
+ </ScrollArea>
265
+
266
+ <MessageInput
267
+ onSend={sendMessage}
268
+ onCancel={status === "running" ? cancelRequest : undefined}
269
+ disabled={status === "running"}
270
+ placeholder={
271
+ status === "running" ? "Claude is thinking..." : "Send a message..."
272
+ }
273
+ />
274
+ </div>
275
+ );
276
+ }
@@ -0,0 +1,195 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { cn } from "@/lib/utils";
5
+ import {
6
+ ChevronRight,
7
+ ChevronDown,
8
+ FolderOpen,
9
+ MoreHorizontal,
10
+ Plus,
11
+ Eye,
12
+ EyeOff,
13
+ Loader2,
14
+ } from "lucide-react";
15
+ import { Button } from "@/components/ui/button";
16
+ import {
17
+ DropdownMenu,
18
+ DropdownMenuContent,
19
+ DropdownMenuItem,
20
+ DropdownMenuTrigger,
21
+ } from "@/components/ui/dropdown-menu";
22
+ import {
23
+ ContextMenu,
24
+ ContextMenuContent,
25
+ ContextMenuItem,
26
+ ContextMenuTrigger,
27
+ } from "@/components/ui/context-menu";
28
+ import { ClaudeSessionCard } from "./ClaudeSessionCard";
29
+ import {
30
+ useClaudeSessionsQuery,
31
+ useHideItem,
32
+ useUnhideItem,
33
+ } from "@/data/claude";
34
+ import type { ClaudeProject } from "@/data/claude";
35
+
36
+ interface ClaudeProjectCardProps {
37
+ project: ClaudeProject;
38
+ showHidden: boolean;
39
+ onSelectSession?: (
40
+ sessionId: string,
41
+ directory: string,
42
+ summary: string,
43
+ projectName: string
44
+ ) => void;
45
+ onNewSession?: (cwd: string, projectName: string) => void;
46
+ }
47
+
48
+ export function ClaudeProjectCard({
49
+ project,
50
+ showHidden,
51
+ onSelectSession,
52
+ onNewSession,
53
+ }: ClaudeProjectCardProps) {
54
+ const [expanded, setExpanded] = useState(false);
55
+ const { data: sessionsData, isPending: isSessionsPending } =
56
+ useClaudeSessionsQuery(expanded ? project.name : null);
57
+ const hideItem = useHideItem();
58
+ const unhideItem = useUnhideItem();
59
+
60
+ const sessions = sessionsData?.sessions || [];
61
+ const filteredSessions = showHidden
62
+ ? sessions
63
+ : sessions.filter((s) => !s.hidden);
64
+
65
+ const handleHideProject = () => {
66
+ hideItem.mutate({ itemType: "project", itemId: project.name });
67
+ };
68
+
69
+ const handleUnhideProject = () => {
70
+ unhideItem.mutate({ itemType: "project", itemId: project.name });
71
+ };
72
+
73
+ const menuContent = project.hidden ? (
74
+ <ContextMenuItem onClick={handleUnhideProject}>
75
+ <Eye className="mr-2 h-3 w-3" />
76
+ Show project
77
+ </ContextMenuItem>
78
+ ) : (
79
+ <ContextMenuItem onClick={handleHideProject}>
80
+ <EyeOff className="mr-2 h-3 w-3" />
81
+ Hide project
82
+ </ContextMenuItem>
83
+ );
84
+
85
+ const cardContent = (
86
+ <div
87
+ onClick={() => setExpanded(!expanded)}
88
+ className={cn(
89
+ "group flex cursor-pointer items-center gap-1 rounded-md px-2 py-1.5 text-sm",
90
+ "min-h-[36px] md:min-h-[28px]",
91
+ "hover:bg-accent/50",
92
+ project.hidden && "opacity-40"
93
+ )}
94
+ >
95
+ <button className="flex-shrink-0 p-0.5">
96
+ {expanded ? (
97
+ <ChevronDown className="text-muted-foreground h-4 w-4" />
98
+ ) : (
99
+ <ChevronRight className="text-muted-foreground h-4 w-4" />
100
+ )}
101
+ </button>
102
+ <FolderOpen className="text-muted-foreground h-3.5 w-3.5 flex-shrink-0" />
103
+ <span className="min-w-0 flex-1 truncate text-sm font-medium">
104
+ {project.displayName}
105
+ </span>
106
+ <span className="text-muted-foreground flex-shrink-0 text-[10px]">
107
+ {project.sessionCount}
108
+ </span>
109
+ {onNewSession && (
110
+ <Button
111
+ variant="ghost"
112
+ size="icon-sm"
113
+ className="h-7 w-7 flex-shrink-0 opacity-100 md:h-6 md:w-6 md:opacity-0 md:group-hover:opacity-100"
114
+ onClick={(e) => {
115
+ e.stopPropagation();
116
+ onNewSession(project.directory || "~", project.name);
117
+ }}
118
+ >
119
+ <Plus className="h-3.5 w-3.5" />
120
+ </Button>
121
+ )}
122
+ <DropdownMenu>
123
+ <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
124
+ <Button
125
+ variant="ghost"
126
+ size="icon-sm"
127
+ className="h-7 w-7 flex-shrink-0 opacity-100 md:h-6 md:w-6 md:opacity-0 md:group-hover:opacity-100"
128
+ >
129
+ <MoreHorizontal className="h-4 w-4" />
130
+ </Button>
131
+ </DropdownMenuTrigger>
132
+ <DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
133
+ {project.hidden ? (
134
+ <DropdownMenuItem onClick={handleUnhideProject}>
135
+ <Eye className="mr-2 h-3 w-3" />
136
+ Show project
137
+ </DropdownMenuItem>
138
+ ) : (
139
+ <DropdownMenuItem onClick={handleHideProject}>
140
+ <EyeOff className="mr-2 h-3 w-3" />
141
+ Hide project
142
+ </DropdownMenuItem>
143
+ )}
144
+ </DropdownMenuContent>
145
+ </DropdownMenu>
146
+ </div>
147
+ );
148
+
149
+ return (
150
+ <div className="space-y-0.5">
151
+ <ContextMenu>
152
+ <ContextMenuTrigger asChild>{cardContent}</ContextMenuTrigger>
153
+ <ContextMenuContent>{menuContent}</ContextMenuContent>
154
+ </ContextMenu>
155
+
156
+ {expanded && (
157
+ <div className="border-border/30 ml-3 space-y-px border-l pl-1.5">
158
+ {isSessionsPending ? (
159
+ <div className="flex items-center gap-2 px-2 py-2">
160
+ <Loader2 className="text-muted-foreground h-3 w-3 animate-spin" />
161
+ <span className="text-muted-foreground text-xs">
162
+ Loading sessions...
163
+ </span>
164
+ </div>
165
+ ) : filteredSessions.length === 0 ? (
166
+ <p className="text-muted-foreground px-2 py-2 text-xs">
167
+ No sessions
168
+ </p>
169
+ ) : (
170
+ filteredSessions.map((session) => (
171
+ <ClaudeSessionCard
172
+ key={session.sessionId}
173
+ session={session}
174
+ projectName={project.name}
175
+ onSelect={onSelectSession}
176
+ onHide={() =>
177
+ hideItem.mutate({
178
+ itemType: "session",
179
+ itemId: session.sessionId,
180
+ })
181
+ }
182
+ onUnhide={() =>
183
+ unhideItem.mutate({
184
+ itemType: "session",
185
+ itemId: session.sessionId,
186
+ })
187
+ }
188
+ />
189
+ ))
190
+ )}
191
+ </div>
192
+ )}
193
+ </div>
194
+ );
195
+ }
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import { useState, useMemo } from "react";
4
+ import { Eye, EyeOff, Loader2 } from "lucide-react";
5
+ import { useClaudeProjectsQuery, useClaudeUpdates } from "@/data/claude";
6
+ import { ClaudeProjectCard } from "./ClaudeProjectCard";
7
+
8
+ interface ClaudeProjectsSectionProps {
9
+ onSelectSession?: (
10
+ sessionId: string,
11
+ directory: string,
12
+ summary: string,
13
+ projectName: string
14
+ ) => void;
15
+ onNewSession?: (cwd: string, projectName: string) => void;
16
+ }
17
+
18
+ function ProjectsSkeleton() {
19
+ return (
20
+ <div className="space-y-1 px-2 py-1">
21
+ {Array.from({ length: 6 }).map((_, i) => (
22
+ <div key={i} className="flex items-center gap-2 rounded-md px-2 py-1.5">
23
+ <div className="bg-muted h-4 w-4 animate-pulse rounded" />
24
+ <div
25
+ className="bg-muted h-3.5 animate-pulse rounded"
26
+ style={{ width: `${60 + Math.random() * 60}px` }}
27
+ />
28
+ <div className="flex-1" />
29
+ <div className="bg-muted h-3 w-4 animate-pulse rounded" />
30
+ </div>
31
+ ))}
32
+ </div>
33
+ );
34
+ }
35
+
36
+ export function ClaudeProjectsSection({
37
+ onSelectSession,
38
+ onNewSession,
39
+ }: ClaudeProjectsSectionProps) {
40
+ useClaudeUpdates();
41
+ const { data: projects = [], isPending } = useClaudeProjectsQuery();
42
+ const [showHidden, setShowHidden] = useState(false);
43
+
44
+ const filteredProjects = useMemo(() => {
45
+ const visible = projects.filter((p) => !p.hidden);
46
+ const hidden = projects.filter((p) => p.hidden);
47
+ if (showHidden) return [...visible, ...hidden];
48
+ return visible;
49
+ }, [projects, showHidden]);
50
+
51
+ const hiddenCount = projects.filter((p) => p.hidden).length;
52
+
53
+ return (
54
+ <div>
55
+ <div className="flex items-center justify-between px-2 py-1">
56
+ <span className="text-muted-foreground text-xs font-medium">
57
+ Projects
58
+ </span>
59
+ <div className="flex items-center gap-1">
60
+ {isPending && (
61
+ <Loader2 className="text-muted-foreground h-3 w-3 animate-spin" />
62
+ )}
63
+ {hiddenCount > 0 && (
64
+ <button
65
+ onClick={() => setShowHidden(!showHidden)}
66
+ className="text-muted-foreground hover:text-foreground p-0.5"
67
+ >
68
+ {showHidden ? <EyeOff size={14} /> : <Eye size={14} />}
69
+ </button>
70
+ )}
71
+ </div>
72
+ </div>
73
+
74
+ {isPending && projects.length === 0 && <ProjectsSkeleton />}
75
+
76
+ <div className="space-y-0.5">
77
+ {filteredProjects.map((project) => (
78
+ <ClaudeProjectCard
79
+ key={project.name}
80
+ project={project}
81
+ showHidden={showHidden}
82
+ onSelectSession={onSelectSession}
83
+ onNewSession={onNewSession}
84
+ />
85
+ ))}
86
+ </div>
87
+ </div>
88
+ );
89
+ }
@@ -0,0 +1,100 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import { MessageSquare, MoreHorizontal, Eye, EyeOff } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuTrigger,
11
+ } from "@/components/ui/dropdown-menu";
12
+ import type { ClaudeSession } from "@/data/claude";
13
+
14
+ interface ClaudeSessionCardProps {
15
+ session: ClaudeSession;
16
+ projectName: string;
17
+ onHide: () => void;
18
+ onUnhide: () => void;
19
+ onSelect?: (
20
+ sessionId: string,
21
+ directory: string,
22
+ summary: string,
23
+ projectName: string
24
+ ) => void;
25
+ }
26
+
27
+ function getTimeAgo(dateStr: string): string {
28
+ const date = new Date(dateStr);
29
+ const now = new Date();
30
+ const diffMs = now.getTime() - date.getTime();
31
+ const diffMins = Math.floor(diffMs / 60000);
32
+
33
+ if (diffMins < 1) return "just now";
34
+ if (diffMins < 60) return `${diffMins}m ago`;
35
+
36
+ const diffHours = Math.floor(diffMins / 60);
37
+ if (diffHours < 24) return `${diffHours}h ago`;
38
+
39
+ const diffDays = Math.floor(diffHours / 24);
40
+ if (diffDays < 7) return `${diffDays}d ago`;
41
+
42
+ return date.toLocaleDateString();
43
+ }
44
+
45
+ export function ClaudeSessionCard({
46
+ session,
47
+ projectName,
48
+ onHide,
49
+ onUnhide,
50
+ onSelect,
51
+ }: ClaudeSessionCardProps) {
52
+ const handleClick = () => {
53
+ if (onSelect && session.cwd) {
54
+ onSelect(session.sessionId, session.cwd, session.summary, projectName);
55
+ }
56
+ };
57
+
58
+ return (
59
+ <div
60
+ onClick={handleClick}
61
+ className={cn(
62
+ "group flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 text-sm",
63
+ "hover:bg-accent/50",
64
+ session.hidden && "opacity-40"
65
+ )}
66
+ >
67
+ <MessageSquare className="text-muted-foreground h-3.5 w-3.5 flex-shrink-0" />
68
+ <span className="flex-1 truncate text-xs">
69
+ {session.summary || "Untitled session"}
70
+ </span>
71
+ <span className="text-muted-foreground flex-shrink-0 text-[10px]">
72
+ {getTimeAgo(session.lastActivity)}
73
+ </span>
74
+ <DropdownMenu>
75
+ <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
76
+ <Button
77
+ variant="ghost"
78
+ size="icon-sm"
79
+ className="h-6 w-6 flex-shrink-0 opacity-0 group-hover:opacity-100"
80
+ >
81
+ <MoreHorizontal className="h-3.5 w-3.5" />
82
+ </Button>
83
+ </DropdownMenuTrigger>
84
+ <DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
85
+ {session.hidden ? (
86
+ <DropdownMenuItem onClick={onUnhide}>
87
+ <Eye className="mr-2 h-3 w-3" />
88
+ Show session
89
+ </DropdownMenuItem>
90
+ ) : (
91
+ <DropdownMenuItem onClick={onHide}>
92
+ <EyeOff className="mr-2 h-3 w-3" />
93
+ Hide session
94
+ </DropdownMenuItem>
95
+ )}
96
+ </DropdownMenuContent>
97
+ </DropdownMenu>
98
+ </div>
99
+ );
100
+ }
@@ -0,0 +1 @@
1
+ export { ClaudeProjectsSection } from "./ClaudeProjectsSection";