@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,173 @@
1
+ import { EventEmitter } from "events";
2
+ import type {
3
+ StreamMessage,
4
+ StreamMessageSystem,
5
+ StreamMessageAssistant,
6
+ StreamMessageResult,
7
+ ClientEvent,
8
+ TextContent,
9
+ } from "./types";
10
+
11
+ export class StreamParser extends EventEmitter {
12
+ private buffer = "";
13
+ private sessionId: string;
14
+
15
+ constructor(sessionId: string) {
16
+ super();
17
+ this.sessionId = sessionId;
18
+ }
19
+
20
+ // Process incoming data chunk
21
+ write(chunk: string): void {
22
+ this.buffer += chunk;
23
+
24
+ // Process complete lines (NDJSON format)
25
+ const lines = this.buffer.split("\n");
26
+ this.buffer = lines.pop() || ""; // Keep incomplete line in buffer
27
+
28
+ for (const line of lines) {
29
+ if (line.trim()) {
30
+ this.parseLine(line);
31
+ }
32
+ }
33
+ }
34
+
35
+ // Flush any remaining buffer
36
+ end(): void {
37
+ if (this.buffer.trim()) {
38
+ this.parseLine(this.buffer);
39
+ }
40
+ this.buffer = "";
41
+ }
42
+
43
+ private parseLine(line: string): void {
44
+ try {
45
+ const message: StreamMessage = JSON.parse(line);
46
+ const event = this.transformToClientEvent(message);
47
+ if (event) {
48
+ this.emit("event", event);
49
+ }
50
+ } catch (err) {
51
+ console.error("Failed to parse stream line:", line, err);
52
+ this.emit("parse_error", { type: "parse_error", line, error: err });
53
+ }
54
+ }
55
+
56
+ private transformToClientEvent(message: StreamMessage): ClientEvent | null {
57
+ const timestamp = new Date().toISOString();
58
+
59
+ switch (message.type) {
60
+ // Handle system init event
61
+ case "system": {
62
+ const sysMsg = message as StreamMessageSystem;
63
+ if (sysMsg.subtype === "init") {
64
+ return {
65
+ type: "init",
66
+ sessionId: this.sessionId,
67
+ timestamp,
68
+ data: { claudeSessionId: sysMsg.session_id || "" },
69
+ };
70
+ }
71
+ return null;
72
+ }
73
+
74
+ // Handle assistant message (actual Claude response)
75
+ case "assistant": {
76
+ const assistantMsg = message as StreamMessageAssistant;
77
+ const msg = assistantMsg.message;
78
+ if (!msg?.content) return null;
79
+
80
+ const textBlocks = msg.content
81
+ .filter((c) => c.type === "text" && c.text)
82
+ .map((c) => c.text || "");
83
+
84
+ if (textBlocks.length > 0) {
85
+ return {
86
+ type: "text",
87
+ sessionId: this.sessionId,
88
+ timestamp,
89
+ data: {
90
+ role: msg.role || "assistant",
91
+ text: textBlocks.join(""),
92
+ content: msg.content.filter(
93
+ (c): c is TextContent => c.type === "text" && !!c.text
94
+ ),
95
+ },
96
+ };
97
+ }
98
+ return null;
99
+ }
100
+
101
+ // Legacy message format (if used)
102
+ case "message": {
103
+ const textBlocks = message.content
104
+ .filter((c): c is TextContent => c.type === "text")
105
+ .map((c) => c.text);
106
+
107
+ if (textBlocks.length > 0) {
108
+ return {
109
+ type: "text",
110
+ sessionId: this.sessionId,
111
+ timestamp,
112
+ data: {
113
+ role: message.role,
114
+ text: textBlocks.join(""),
115
+ content: message.content,
116
+ },
117
+ };
118
+ }
119
+ return null;
120
+ }
121
+
122
+ case "tool_use":
123
+ return {
124
+ type: "tool_start",
125
+ sessionId: this.sessionId,
126
+ timestamp,
127
+ data: {
128
+ toolName: message.tool_name,
129
+ input: message.tool_input,
130
+ },
131
+ };
132
+
133
+ case "tool_result":
134
+ return {
135
+ type: "tool_end",
136
+ sessionId: this.sessionId,
137
+ timestamp,
138
+ data: {
139
+ toolName: message.tool_name,
140
+ output: message.output,
141
+ status: message.status,
142
+ },
143
+ };
144
+
145
+ case "result": {
146
+ const resultMsg = message as StreamMessageResult;
147
+ if (resultMsg.subtype === "success" || resultMsg.status === "success") {
148
+ return {
149
+ type: "complete",
150
+ sessionId: this.sessionId,
151
+ timestamp,
152
+ data: {
153
+ durationMs: resultMsg.duration_ms,
154
+ output: resultMsg.result || resultMsg.output,
155
+ },
156
+ };
157
+ } else {
158
+ return {
159
+ type: "error",
160
+ sessionId: this.sessionId,
161
+ timestamp,
162
+ data: {
163
+ error: resultMsg.error || "Unknown error",
164
+ },
165
+ };
166
+ }
167
+ }
168
+
169
+ default:
170
+ return null;
171
+ }
172
+ }
173
+ }
@@ -0,0 +1,154 @@
1
+ // Stream-JSON message types from Claude CLI
2
+
3
+ export interface StreamMessageInit {
4
+ type: "init";
5
+ session_id: string;
6
+ timestamp: string;
7
+ }
8
+
9
+ export interface StreamMessageSystem {
10
+ type: "system";
11
+ subtype: "init" | string;
12
+ session_id?: string;
13
+ timestamp?: string;
14
+ }
15
+
16
+ export interface StreamMessageAssistant {
17
+ type: "assistant";
18
+ message: {
19
+ role: string;
20
+ content: Array<{ type: string; text?: string }>;
21
+ };
22
+ timestamp?: string;
23
+ }
24
+
25
+ export interface StreamMessageContent {
26
+ type: "message";
27
+ role: "assistant" | "user";
28
+ content: ContentBlock[];
29
+ timestamp: string;
30
+ }
31
+
32
+ export interface StreamMessageToolUse {
33
+ type: "tool_use";
34
+ tool_name: string;
35
+ tool_input: Record<string, unknown>;
36
+ timestamp: string;
37
+ }
38
+
39
+ export interface StreamMessageToolResult {
40
+ type: "tool_result";
41
+ tool_name: string;
42
+ output: string;
43
+ status: "success" | "error";
44
+ timestamp: string;
45
+ }
46
+
47
+ export interface StreamMessageResult {
48
+ type: "result";
49
+ subtype?: "success" | "error";
50
+ status?: "success" | "error";
51
+ duration_ms?: number;
52
+ result?: string;
53
+ output?: string;
54
+ error?: string;
55
+ timestamp?: string;
56
+ }
57
+
58
+ export type StreamMessage =
59
+ | StreamMessageInit
60
+ | StreamMessageSystem
61
+ | StreamMessageAssistant
62
+ | StreamMessageContent
63
+ | StreamMessageToolUse
64
+ | StreamMessageToolResult
65
+ | StreamMessageResult;
66
+
67
+ // Content block types
68
+ export interface TextContent {
69
+ type: "text";
70
+ text: string;
71
+ }
72
+
73
+ export interface ToolUseContent {
74
+ type: "tool_use";
75
+ id: string;
76
+ name: string;
77
+ input: Record<string, unknown>;
78
+ }
79
+
80
+ export interface ToolResultContent {
81
+ type: "tool_result";
82
+ tool_use_id: string;
83
+ content: string;
84
+ }
85
+
86
+ export type ContentBlock = TextContent | ToolUseContent | ToolResultContent;
87
+
88
+ // Client events (sent to WebSocket clients)
89
+ export interface ClientEventInit {
90
+ type: "init";
91
+ sessionId: string;
92
+ timestamp: string;
93
+ data: { claudeSessionId: string };
94
+ }
95
+
96
+ export interface ClientEventText {
97
+ type: "text";
98
+ sessionId: string;
99
+ timestamp: string;
100
+ data: { role: string; text: string; content: ContentBlock[] };
101
+ }
102
+
103
+ export interface ClientEventToolStart {
104
+ type: "tool_start";
105
+ sessionId: string;
106
+ timestamp: string;
107
+ data: { toolName: string; input: Record<string, unknown> };
108
+ }
109
+
110
+ export interface ClientEventToolEnd {
111
+ type: "tool_end";
112
+ sessionId: string;
113
+ timestamp: string;
114
+ data: { toolName: string; output: string; status: string };
115
+ }
116
+
117
+ export interface ClientEventComplete {
118
+ type: "complete";
119
+ sessionId: string;
120
+ timestamp: string;
121
+ data: { durationMs?: number; output?: string };
122
+ }
123
+
124
+ export interface ClientEventError {
125
+ type: "error";
126
+ sessionId: string;
127
+ timestamp: string;
128
+ data: { error: string };
129
+ }
130
+
131
+ export interface ClientEventStatus {
132
+ type: "status";
133
+ sessionId: string;
134
+ timestamp: string;
135
+ data: { status: string; exitCode?: number };
136
+ }
137
+
138
+ export type ClientEvent =
139
+ | ClientEventInit
140
+ | ClientEventText
141
+ | ClientEventToolStart
142
+ | ClientEventToolEnd
143
+ | ClientEventComplete
144
+ | ClientEventError
145
+ | ClientEventStatus;
146
+
147
+ // Session options
148
+ export interface ClaudeSessionOptions {
149
+ model?: string;
150
+ workingDirectory?: string;
151
+ resume?: boolean; // Use --continue or --resume
152
+ claudeSessionId?: string; // For --resume
153
+ systemPrompt?: string;
154
+ }
@@ -0,0 +1,71 @@
1
+ import { watch } from "chokidar";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { WebSocket } from "ws";
5
+ import { invalidateProject, invalidateAll } from "./jsonl-cache";
6
+
7
+ const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects");
8
+
9
+ const updateClients = new Set<WebSocket>();
10
+
11
+ export function addUpdateClient(ws: WebSocket): void {
12
+ updateClients.add(ws);
13
+ ws.on("close", () => updateClients.delete(ws));
14
+ }
15
+
16
+ function broadcast(msg: object): void {
17
+ const data = JSON.stringify(msg);
18
+ for (const ws of updateClients) {
19
+ if (ws.readyState === WebSocket.OPEN) {
20
+ ws.send(data);
21
+ }
22
+ }
23
+ }
24
+
25
+ const debounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
26
+
27
+ function handleFileChange(filePath: string): void {
28
+ const relative = path.relative(CLAUDE_PROJECTS_DIR, filePath);
29
+ const projectName = relative.split(path.sep)[0];
30
+ if (!projectName) return;
31
+
32
+ const existing = debounceTimers.get(projectName);
33
+ if (existing) clearTimeout(existing);
34
+
35
+ debounceTimers.set(
36
+ projectName,
37
+ setTimeout(() => {
38
+ debounceTimers.delete(projectName);
39
+ invalidateProject(projectName);
40
+ broadcast({ type: "project-updated", projectName });
41
+ }, 150)
42
+ );
43
+ }
44
+
45
+ export function startWatcher(): void {
46
+ try {
47
+ const watcher = watch(CLAUDE_PROJECTS_DIR, {
48
+ ignoreInitial: true,
49
+ depth: 2,
50
+ ignored: [/node_modules/, /\.git/, /subagents/],
51
+ });
52
+
53
+ watcher.on("change", handleFileChange);
54
+ watcher.on("add", (fp) => {
55
+ handleFileChange(fp);
56
+ const relative = path.relative(CLAUDE_PROJECTS_DIR, fp);
57
+ if (!relative.includes(path.sep)) {
58
+ invalidateAll();
59
+ broadcast({ type: "projects-changed" });
60
+ }
61
+ });
62
+ watcher.on("addDir", () => {
63
+ invalidateAll();
64
+ broadcast({ type: "projects-changed" });
65
+ });
66
+
67
+ console.log("> File watcher started on ~/.claude/projects/");
68
+ } catch (err) {
69
+ console.error("Failed to start file watcher:", err);
70
+ }
71
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Client-side registry for preserving terminal state across navigation
3
+ * Stores scroll positions and other ephemeral state that should persist
4
+ * when switching between tabs/sessions.
5
+ */
6
+
7
+ interface TerminalState {
8
+ scrollTop: number;
9
+ scrollHeight: number;
10
+ lastActivity: number;
11
+ cursorY: number;
12
+ }
13
+
14
+ interface SessionEntry {
15
+ tabId: string;
16
+ sessionId?: string;
17
+ attachedTmux?: string;
18
+ terminalState?: TerminalState;
19
+ }
20
+
21
+ class SessionRegistry {
22
+ private sessions: Map<string, SessionEntry> = new Map();
23
+
24
+ /**
25
+ * Generate a unique key for a pane+tab combination
26
+ */
27
+ private getKey(paneId: string, tabId: string): string {
28
+ return `${paneId}:${tabId}`;
29
+ }
30
+
31
+ /**
32
+ * Register or update a session entry
33
+ */
34
+ register(
35
+ paneId: string,
36
+ tabId: string,
37
+ data: Partial<Omit<SessionEntry, "tabId">>
38
+ ): void {
39
+ const key = this.getKey(paneId, tabId);
40
+ const existing = this.sessions.get(key);
41
+
42
+ this.sessions.set(key, {
43
+ tabId,
44
+ ...existing,
45
+ ...data,
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Get session entry
51
+ */
52
+ get(paneId: string, tabId: string): SessionEntry | undefined {
53
+ return this.sessions.get(this.getKey(paneId, tabId));
54
+ }
55
+
56
+ /**
57
+ * Save terminal state (scroll position, cursor, etc.)
58
+ */
59
+ saveTerminalState(paneId: string, tabId: string, state: TerminalState): void {
60
+ const key = this.getKey(paneId, tabId);
61
+ const existing = this.sessions.get(key);
62
+
63
+ this.sessions.set(key, {
64
+ tabId,
65
+ ...existing,
66
+ terminalState: state,
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Get saved terminal state
72
+ */
73
+ getTerminalState(paneId: string, tabId: string): TerminalState | undefined {
74
+ return this.sessions.get(this.getKey(paneId, tabId))?.terminalState;
75
+ }
76
+
77
+ /**
78
+ * Remove session entry
79
+ */
80
+ remove(paneId: string, tabId: string): void {
81
+ this.sessions.delete(this.getKey(paneId, tabId));
82
+ }
83
+
84
+ /**
85
+ * Clear all entries for a pane
86
+ */
87
+ clearPane(paneId: string): void {
88
+ for (const key of this.sessions.keys()) {
89
+ if (key.startsWith(`${paneId}:`)) {
90
+ this.sessions.delete(key);
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Clear all entries
97
+ */
98
+ clear(): void {
99
+ this.sessions.clear();
100
+ }
101
+
102
+ /**
103
+ * Get count of active sessions
104
+ */
105
+ get size(): number {
106
+ return this.sessions.size;
107
+ }
108
+ }
109
+
110
+ // Singleton instance
111
+ export const sessionRegistry = new SessionRegistry();
@@ -0,0 +1,121 @@
1
+ import { execSync } from "child_process";
2
+
3
+ /**
4
+ * Check if ripgrep is available on the system
5
+ */
6
+ export function isRipgrepAvailable(): boolean {
7
+ try {
8
+ execSync("which rg", { encoding: "utf-8", stdio: "pipe" });
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ export interface SearchOptions {
16
+ maxResults?: number;
17
+ contextLines?: number;
18
+ filePattern?: string;
19
+ caseSensitive?: boolean;
20
+ }
21
+
22
+ export interface SearchMatch {
23
+ type: "match";
24
+ data: {
25
+ path: { text: string };
26
+ lines: { text: string };
27
+ line_number: number;
28
+ absolute_offset: number;
29
+ submatches: Array<{ match: { text: string }; start: number; end: number }>;
30
+ };
31
+ }
32
+
33
+ export interface FormattedMatch {
34
+ file: string;
35
+ line: number;
36
+ column: number;
37
+ matchText: string;
38
+ lineText: string;
39
+ }
40
+
41
+ export function searchCode(
42
+ workingDir: string,
43
+ query: string,
44
+ options: SearchOptions = {}
45
+ ): SearchMatch[] {
46
+ const {
47
+ maxResults = 100,
48
+ contextLines = 2,
49
+ filePattern = "*",
50
+ caseSensitive = false,
51
+ } = options;
52
+
53
+ try {
54
+ // Use spawn instead of execSync for better control
55
+ const { spawnSync } = require("child_process");
56
+
57
+ const args = [
58
+ "--json",
59
+ `--max-count=${Math.ceil(maxResults / 10)}`,
60
+ `--context=${contextLines}`,
61
+ "--ignore-case",
62
+ query,
63
+ ".", // CRITICAL: Tell ripgrep to search current directory explicitly
64
+ ];
65
+
66
+ const result = spawnSync("rg", args, {
67
+ cwd: workingDir,
68
+ encoding: "utf-8",
69
+ timeout: 10000,
70
+ maxBuffer: 1024 * 1024 * 5,
71
+ stdio: ["ignore", "pipe", "pipe"], // Ignore stdin so ripgrep doesn't wait for it
72
+ });
73
+
74
+ if (result.error) {
75
+ throw result.error;
76
+ }
77
+
78
+ // Status 1 = no matches (not an error for ripgrep)
79
+ if (result.status !== 0 && result.status !== 1) {
80
+ return [];
81
+ }
82
+
83
+ const output = result.stdout || "";
84
+ const matches: SearchMatch[] = [];
85
+ const lines = output.trim().split("\n").filter(Boolean);
86
+
87
+ for (const line of lines) {
88
+ try {
89
+ const parsed = JSON.parse(line);
90
+ if (parsed.type === "match") {
91
+ matches.push(parsed);
92
+ if (matches.length >= maxResults) break;
93
+ }
94
+ } catch {
95
+ continue;
96
+ }
97
+ }
98
+
99
+ return matches;
100
+ } catch (error) {
101
+ console.error("Error in searchCode:", error);
102
+ // ENOENT = command not found
103
+ if ((error as any).code === "ENOENT") {
104
+ throw new Error(
105
+ "ripgrep (rg) not found. Install with: brew install ripgrep"
106
+ );
107
+ }
108
+ // Other errors - return empty
109
+ return [];
110
+ }
111
+ }
112
+
113
+ export function formatSearchResults(matches: SearchMatch[]): FormattedMatch[] {
114
+ return matches.map((match) => ({
115
+ file: match.data.path.text,
116
+ line: match.data.line_number,
117
+ column: match.data.submatches[0]?.start || 0,
118
+ matchText: match.data.submatches[0]?.match.text || "",
119
+ lineText: match.data.lines.text,
120
+ }));
121
+ }
@@ -0,0 +1,48 @@
1
+ import Database from "better-sqlite3";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { createSchema } from "./schema";
5
+ import { runMigrations } from "./migrations";
6
+
7
+ export * from "./types";
8
+ export { queries } from "./queries";
9
+
10
+ const DB_DIR = path.join(os.homedir(), ".claude-deck");
11
+ const DB_PATH = process.env.DB_PATH || path.join(DB_DIR, "data.db");
12
+
13
+ let _db: Database.Database | null = null;
14
+
15
+ export function getDb(): Database.Database {
16
+ if (!_db) {
17
+ const fs = require("fs");
18
+ fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
19
+
20
+ _db = new Database(DB_PATH);
21
+ _db.pragma("journal_mode = WAL");
22
+ _db.pragma("foreign_keys = ON");
23
+ _db.pragma("busy_timeout = 5000");
24
+ }
25
+ return _db;
26
+ }
27
+
28
+ let _initialized = false;
29
+
30
+ export async function initDb(): Promise<Database.Database> {
31
+ const db = getDb();
32
+
33
+ if (!_initialized) {
34
+ createSchema(db);
35
+ runMigrations(db);
36
+ _initialized = true;
37
+ }
38
+
39
+ return db;
40
+ }
41
+
42
+ export async function closeDb(): Promise<void> {
43
+ if (_db) {
44
+ _db.close();
45
+ _db = null;
46
+ _initialized = false;
47
+ }
48
+ }