@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,257 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback, useRef } from "react";
4
+ import { toast } from "sonner";
5
+ import {
6
+ NotificationSettings,
7
+ NotificationEvent,
8
+ defaultSettings,
9
+ loadSettings,
10
+ saveSettings,
11
+ requestNotificationPermission,
12
+ canSendBrowserNotification,
13
+ sendBrowserNotification,
14
+ playNotificationSound,
15
+ setTabNotificationCount,
16
+ flashTabTitle,
17
+ clearTabNotifications,
18
+ } from "@/lib/notifications";
19
+
20
+ type SessionStatus = "idle" | "running" | "waiting" | "error" | "dead";
21
+
22
+ interface SessionState {
23
+ id: string;
24
+ name: string;
25
+ status: SessionStatus;
26
+ }
27
+
28
+ interface UseNotificationsOptions {
29
+ onSessionClick?: (sessionId: string) => void;
30
+ }
31
+
32
+ export function useNotifications(options: UseNotificationsOptions = {}) {
33
+ const { onSessionClick } = options;
34
+ const [settings, setSettings] =
35
+ useState<NotificationSettings>(defaultSettings);
36
+ const [permissionGranted, setPermissionGranted] = useState(false);
37
+ const previousStates = useRef<Map<string, SessionStatus>>(new Map());
38
+ const waitingCount = useRef(0);
39
+ // Track which sessions have been notified to prevent duplicates
40
+ const notifiedSessions = useRef<Set<string>>(new Set());
41
+
42
+ // Load settings on mount
43
+ useEffect(() => {
44
+ setSettings(loadSettings());
45
+ setPermissionGranted(canSendBrowserNotification());
46
+ }, []);
47
+
48
+ // Request permission
49
+ const requestPermission = useCallback(async () => {
50
+ const granted = await requestNotificationPermission();
51
+ setPermissionGranted(granted);
52
+ return granted;
53
+ }, []);
54
+
55
+ // Update settings
56
+ const updateSettings = useCallback(
57
+ (newSettings: Partial<NotificationSettings>) => {
58
+ setSettings((prev) => {
59
+ const updated = { ...prev, ...newSettings };
60
+ saveSettings(updated);
61
+ return updated;
62
+ });
63
+ },
64
+ []
65
+ );
66
+
67
+ // Toggle a specific event
68
+ const toggleEvent = useCallback(
69
+ (event: NotificationEvent, enabled: boolean) => {
70
+ setSettings((prev) => {
71
+ const updated = {
72
+ ...prev,
73
+ events: { ...prev.events, [event]: enabled },
74
+ };
75
+ saveSettings(updated);
76
+ return updated;
77
+ });
78
+ },
79
+ []
80
+ );
81
+
82
+ // Send notification for an event
83
+ const notify = useCallback(
84
+ (
85
+ event: NotificationEvent,
86
+ sessionId: string,
87
+ sessionName: string,
88
+ message?: string
89
+ ) => {
90
+ if (!settings.enabled || !settings.events[event]) return;
91
+
92
+ const titles: Record<NotificationEvent, string> = {
93
+ waiting: `${sessionName} needs input`,
94
+ error: `${sessionName} encountered an error`,
95
+ completed: `${sessionName} completed`,
96
+ };
97
+
98
+ const title = titles[event];
99
+ const body = message || getDefaultMessage(event);
100
+
101
+ // In-app toast with click action
102
+ const toastTypes: Record<
103
+ NotificationEvent,
104
+ "warning" | "error" | "success"
105
+ > = {
106
+ waiting: "warning",
107
+ error: "error",
108
+ completed: "success",
109
+ };
110
+ toast[toastTypes[event]](title, {
111
+ description: body,
112
+ action: {
113
+ label: "Go to session",
114
+ onClick: () => onSessionClick?.(sessionId),
115
+ },
116
+ });
117
+
118
+ // Browser notification (only if page not focused)
119
+ if (settings.browserNotifications && permissionGranted) {
120
+ sendBrowserNotification(
121
+ title,
122
+ { body, tag: `agentos-${event}-${sessionName}` },
123
+ () => onSessionClick?.(sessionId)
124
+ );
125
+ }
126
+
127
+ // Sound
128
+ if (settings.sound) {
129
+ playNotificationSound(event);
130
+ }
131
+
132
+ // Flash tab title
133
+ if (event === "waiting") {
134
+ flashTabTitle(`Waiting: ${sessionName}`);
135
+ }
136
+ },
137
+ [settings, permissionGranted, onSessionClick]
138
+ );
139
+
140
+ // Check for state changes and notify
141
+ const checkStateChanges = useCallback(
142
+ (sessions: SessionState[], activeSessionId?: string | null) => {
143
+ if (!settings.enabled) return;
144
+
145
+ let newWaitingCount = 0;
146
+
147
+ sessions.forEach((session) => {
148
+ const prevStatus = previousStates.current.get(session.id);
149
+ const currentStatus = session.status;
150
+
151
+ // Track waiting count
152
+ if (currentStatus === "waiting") {
153
+ newWaitingCount++;
154
+ }
155
+
156
+ // Skip if no previous state (initial load)
157
+ if (prevStatus === undefined) {
158
+ previousStates.current.set(session.id, currentStatus);
159
+ return;
160
+ }
161
+
162
+ // Skip if status unchanged
163
+ if (prevStatus === currentStatus) return;
164
+
165
+ // Skip notifications for the currently active/focused session
166
+ if (session.id === activeSessionId) {
167
+ previousStates.current.set(session.id, currentStatus);
168
+ return;
169
+ }
170
+
171
+ // Detect transitions and notify (with deduplication)
172
+ const notifyKey = `${session.id}-${currentStatus}`;
173
+
174
+ if (currentStatus === "waiting" && prevStatus !== "waiting") {
175
+ if (!notifiedSessions.current.has(notifyKey)) {
176
+ notifiedSessions.current.add(notifyKey);
177
+ notify("waiting", session.id, session.name);
178
+ }
179
+ } else if (currentStatus === "error" && prevStatus !== "error") {
180
+ if (!notifiedSessions.current.has(notifyKey)) {
181
+ notifiedSessions.current.add(notifyKey);
182
+ notify("error", session.id, session.name);
183
+ }
184
+ } else if (
185
+ currentStatus === "idle" &&
186
+ (prevStatus === "running" || prevStatus === "waiting")
187
+ ) {
188
+ const completedKey = `${session.id}-completed`;
189
+ if (!notifiedSessions.current.has(completedKey)) {
190
+ notifiedSessions.current.add(completedKey);
191
+ notify("completed", session.id, session.name);
192
+ }
193
+ }
194
+
195
+ // Clear notification tracking when status changes away from notified state
196
+ if (prevStatus !== currentStatus) {
197
+ notifiedSessions.current.delete(`${session.id}-${prevStatus}`);
198
+ if (prevStatus === "idle") {
199
+ notifiedSessions.current.delete(`${session.id}-completed`);
200
+ }
201
+ }
202
+
203
+ previousStates.current.set(session.id, currentStatus);
204
+ });
205
+
206
+ // Update tab badge
207
+ if (newWaitingCount !== waitingCount.current) {
208
+ waitingCount.current = newWaitingCount;
209
+ setTabNotificationCount(newWaitingCount);
210
+ }
211
+ },
212
+ [settings.enabled, notify]
213
+ );
214
+
215
+ // Clear notifications when focused
216
+ useEffect(() => {
217
+ const handleFocus = () => {
218
+ // Don't clear count, just stop flashing
219
+ };
220
+
221
+ const handleVisibilityChange = () => {
222
+ if (document.visibilityState === "visible") {
223
+ // User returned to tab
224
+ }
225
+ };
226
+
227
+ window.addEventListener("focus", handleFocus);
228
+ document.addEventListener("visibilitychange", handleVisibilityChange);
229
+
230
+ return () => {
231
+ window.removeEventListener("focus", handleFocus);
232
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
233
+ clearTabNotifications();
234
+ };
235
+ }, []);
236
+
237
+ return {
238
+ settings,
239
+ permissionGranted,
240
+ requestPermission,
241
+ updateSettings,
242
+ toggleEvent,
243
+ notify,
244
+ checkStateChanges,
245
+ };
246
+ }
247
+
248
+ function getDefaultMessage(event: NotificationEvent): string {
249
+ switch (event) {
250
+ case "waiting":
251
+ return "Session is waiting for your input";
252
+ case "error":
253
+ return "Something went wrong";
254
+ case "completed":
255
+ return "Task has finished";
256
+ }
257
+ }
@@ -0,0 +1,53 @@
1
+ import { useCallback } from "react";
2
+ import {
3
+ useProjectsQuery,
4
+ useToggleProject,
5
+ useDeleteProject,
6
+ useRenameProject,
7
+ } from "@/data/projects";
8
+
9
+ export function useProjects() {
10
+ const { data: projects = [], refetch } = useProjectsQuery();
11
+ const toggleMutation = useToggleProject();
12
+ const deleteMutation = useDeleteProject();
13
+ const renameMutation = useRenameProject();
14
+
15
+ const toggleProject = useCallback(
16
+ async (projectId: string, expanded: boolean) => {
17
+ await toggleMutation.mutateAsync({ projectId, expanded });
18
+ },
19
+ [toggleMutation]
20
+ );
21
+
22
+ const deleteProject = useCallback(
23
+ async (projectId: string) => {
24
+ if (
25
+ !confirm(
26
+ "Delete this project? Sessions will be moved to Uncategorized."
27
+ )
28
+ )
29
+ return;
30
+ await deleteMutation.mutateAsync(projectId);
31
+ },
32
+ [deleteMutation]
33
+ );
34
+
35
+ const renameProject = useCallback(
36
+ async (projectId: string, newName: string) => {
37
+ await renameMutation.mutateAsync({ projectId, newName });
38
+ },
39
+ [renameMutation]
40
+ );
41
+
42
+ const fetchProjects = useCallback(async () => {
43
+ await refetch();
44
+ }, [refetch]);
45
+
46
+ return {
47
+ projects,
48
+ fetchProjects,
49
+ toggleProject,
50
+ deleteProject,
51
+ renameProject,
52
+ };
53
+ }
@@ -0,0 +1,30 @@
1
+ import type { Session } from "@/lib/db";
2
+ import type { SessionStatus } from "@/components/views/types";
3
+ import { useSessionStatusesQuery } from "@/data/statuses";
4
+
5
+ interface UseSessionStatusesOptions {
6
+ sessions: Session[];
7
+ activeSessionId?: string | null;
8
+ checkStateChanges: (
9
+ states: Array<{
10
+ id: string;
11
+ name: string;
12
+ status: SessionStatus["status"];
13
+ }>,
14
+ activeSessionId?: string | null
15
+ ) => void;
16
+ }
17
+
18
+ export function useSessionStatuses({
19
+ sessions,
20
+ activeSessionId,
21
+ checkStateChanges,
22
+ }: UseSessionStatusesOptions) {
23
+ const { sessionStatuses } = useSessionStatusesQuery({
24
+ sessions,
25
+ activeSessionId,
26
+ checkStateChanges,
27
+ });
28
+
29
+ return { sessionStatuses };
30
+ }
@@ -0,0 +1,86 @@
1
+ import { useCallback } from "react";
2
+ import type { Session } from "@/lib/db";
3
+ import {
4
+ useSessionsQuery,
5
+ useDeleteSession,
6
+ useRenameSession,
7
+ useForkSession,
8
+ useSummarizeSession,
9
+ useMoveSessionToGroup,
10
+ useMoveSessionToProject,
11
+ } from "@/data/sessions";
12
+
13
+ export function useSessions() {
14
+ const { data, refetch } = useSessionsQuery();
15
+ const sessions = data?.sessions ?? [];
16
+ const groups = data?.groups ?? [];
17
+
18
+ const deleteMutation = useDeleteSession();
19
+ const renameMutation = useRenameSession();
20
+ const forkMutation = useForkSession();
21
+ const summarizeMutation = useSummarizeSession();
22
+ const moveToGroupMutation = useMoveSessionToGroup();
23
+ const moveToProjectMutation = useMoveSessionToProject();
24
+
25
+ const fetchSessions = useCallback(async () => {
26
+ await refetch();
27
+ }, [refetch]);
28
+
29
+ const deleteSession = useCallback(
30
+ async (sessionId: string) => {
31
+ if (!confirm("Delete this session? This cannot be undone.")) return;
32
+ await deleteMutation.mutateAsync(sessionId);
33
+ },
34
+ [deleteMutation]
35
+ );
36
+
37
+ const renameSession = useCallback(
38
+ async (sessionId: string, newName: string) => {
39
+ await renameMutation.mutateAsync({ sessionId, newName });
40
+ },
41
+ [renameMutation]
42
+ );
43
+
44
+ const forkSession = useCallback(
45
+ async (sessionId: string): Promise<Session | null> => {
46
+ return await forkMutation.mutateAsync(sessionId);
47
+ },
48
+ [forkMutation]
49
+ );
50
+
51
+ const summarizeSession = useCallback(
52
+ async (sessionId: string): Promise<Session | null> => {
53
+ return await summarizeMutation.mutateAsync(sessionId);
54
+ },
55
+ [summarizeMutation]
56
+ );
57
+
58
+ const moveSessionToGroup = useCallback(
59
+ async (sessionId: string, groupPath: string) => {
60
+ await moveToGroupMutation.mutateAsync({ sessionId, groupPath });
61
+ },
62
+ [moveToGroupMutation]
63
+ );
64
+
65
+ const moveSessionToProject = useCallback(
66
+ async (sessionId: string, projectId: string) => {
67
+ await moveToProjectMutation.mutateAsync({ sessionId, projectId });
68
+ },
69
+ [moveToProjectMutation]
70
+ );
71
+
72
+ return {
73
+ sessions,
74
+ groups,
75
+ summarizingSessionId: summarizeMutation.isPending
76
+ ? (summarizeMutation.variables as string)
77
+ : null,
78
+ fetchSessions,
79
+ deleteSession,
80
+ renameSession,
81
+ forkSession,
82
+ summarizeSession,
83
+ moveSessionToGroup,
84
+ moveSessionToProject,
85
+ };
86
+ }
@@ -0,0 +1,124 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useCallback, useEffect } from "react";
4
+
5
+ // Web Speech API types
6
+ interface SpeechRecognitionEvent extends Event {
7
+ results: SpeechRecognitionResultList;
8
+ resultIndex: number;
9
+ }
10
+
11
+ interface SpeechRecognitionResultList {
12
+ length: number;
13
+ item(index: number): SpeechRecognitionResult;
14
+ [index: number]: SpeechRecognitionResult;
15
+ }
16
+
17
+ interface SpeechRecognitionResult {
18
+ isFinal: boolean;
19
+ length: number;
20
+ item(index: number): SpeechRecognitionAlternative;
21
+ [index: number]: SpeechRecognitionAlternative;
22
+ }
23
+
24
+ interface SpeechRecognitionAlternative {
25
+ transcript: string;
26
+ confidence: number;
27
+ }
28
+
29
+ interface SpeechRecognition extends EventTarget {
30
+ continuous: boolean;
31
+ interimResults: boolean;
32
+ lang: string;
33
+ onresult: ((event: SpeechRecognitionEvent) => void) | null;
34
+ onend: (() => void) | null;
35
+ onerror: ((event: Event) => void) | null;
36
+ onstart: (() => void) | null;
37
+ start(): void;
38
+ stop(): void;
39
+ abort(): void;
40
+ }
41
+
42
+ declare global {
43
+ interface Window {
44
+ SpeechRecognition: new () => SpeechRecognition;
45
+ webkitSpeechRecognition: new () => SpeechRecognition;
46
+ }
47
+ }
48
+
49
+ export function useSpeechRecognition(
50
+ onTranscript: (text: string, isFinal: boolean) => void
51
+ ) {
52
+ const [isListening, setIsListening] = useState(false);
53
+ const [isSupported, setIsSupported] = useState(false);
54
+ const recognitionRef = useRef<SpeechRecognition | null>(null);
55
+ const lastTranscriptRef = useRef("");
56
+
57
+ useEffect(() => {
58
+ // Check for browser support
59
+ const SpeechRecognitionAPI =
60
+ window.SpeechRecognition || window.webkitSpeechRecognition;
61
+ setIsSupported(!!SpeechRecognitionAPI);
62
+
63
+ if (SpeechRecognitionAPI) {
64
+ const recognition = new SpeechRecognitionAPI();
65
+ recognition.continuous = true;
66
+ recognition.interimResults = true;
67
+ recognition.lang = "en-US";
68
+
69
+ recognition.onstart = () => {
70
+ setIsListening(true);
71
+ lastTranscriptRef.current = "";
72
+ };
73
+
74
+ recognition.onend = () => {
75
+ setIsListening(false);
76
+ };
77
+
78
+ recognition.onerror = () => {
79
+ setIsListening(false);
80
+ };
81
+
82
+ recognition.onresult = (event: SpeechRecognitionEvent) => {
83
+ let finalTranscript = "";
84
+
85
+ for (let i = event.resultIndex; i < event.results.length; i++) {
86
+ const transcript = event.results[i][0].transcript;
87
+ if (event.results[i].isFinal) {
88
+ finalTranscript += transcript;
89
+ }
90
+ }
91
+
92
+ // Only send new final transcripts
93
+ if (finalTranscript && finalTranscript !== lastTranscriptRef.current) {
94
+ lastTranscriptRef.current = finalTranscript;
95
+ onTranscript(finalTranscript, true);
96
+ }
97
+ };
98
+
99
+ recognitionRef.current = recognition;
100
+ }
101
+
102
+ return () => {
103
+ if (recognitionRef.current) {
104
+ recognitionRef.current.abort();
105
+ }
106
+ };
107
+ }, [onTranscript]);
108
+
109
+ const toggle = useCallback(() => {
110
+ if (!recognitionRef.current) return;
111
+
112
+ if (isListening) {
113
+ recognitionRef.current.stop();
114
+ } else {
115
+ try {
116
+ recognitionRef.current.start();
117
+ } catch {
118
+ // Already started, ignore
119
+ }
120
+ }
121
+ }, [isListening]);
122
+
123
+ return { isListening, isSupported, toggle };
124
+ }
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+
5
+ /**
6
+ * Mobile-first viewport detection hook
7
+ * Breakpoint: 768px (md in Tailwind)
8
+ */
9
+ export function useViewport() {
10
+ const [isMobile, setIsMobile] = useState(false);
11
+ const [isHydrated, setIsHydrated] = useState(false);
12
+
13
+ useEffect(() => {
14
+ const checkViewport = () => {
15
+ setIsMobile(window.innerWidth < 768);
16
+ };
17
+
18
+ // Initial check
19
+ checkViewport();
20
+ setIsHydrated(true);
21
+
22
+ // Listen for resize
23
+ window.addEventListener("resize", checkViewport);
24
+ return () => window.removeEventListener("resize", checkViewport);
25
+ }, []);
26
+
27
+ return {
28
+ isMobile,
29
+ isDesktop: !isMobile,
30
+ isHydrated, // For avoiding hydration mismatches
31
+ };
32
+ }
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+
5
+ /**
6
+ * Hook to set CSS custom property for actual viewport height.
7
+ * Handles iOS Safari's virtual keyboard by using visualViewport API.
8
+ *
9
+ * Sets --app-height CSS variable on document root that updates when:
10
+ * - Window resizes
11
+ * - Visual viewport changes (keyboard appears/disappears)
12
+ * - Orientation changes
13
+ */
14
+ export function useViewportHeight() {
15
+ useEffect(() => {
16
+ const setAppHeight = () => {
17
+ // Use visualViewport if available (more accurate on mobile with keyboard)
18
+ const vh = window.visualViewport?.height ?? window.innerHeight;
19
+ document.documentElement.style.setProperty("--app-height", `${vh}px`);
20
+ };
21
+
22
+ // Set initial value
23
+ setAppHeight();
24
+
25
+ // Update on window resize
26
+ window.addEventListener("resize", setAppHeight);
27
+
28
+ // Visual viewport resize handles keyboard appearance on mobile
29
+ if (window.visualViewport) {
30
+ window.visualViewport.addEventListener("resize", setAppHeight);
31
+ window.visualViewport.addEventListener("scroll", setAppHeight);
32
+ }
33
+
34
+ // Handle orientation changes
35
+ if ("orientation" in screen) {
36
+ screen.orientation.addEventListener("change", setAppHeight);
37
+ }
38
+
39
+ return () => {
40
+ window.removeEventListener("resize", setAppHeight);
41
+ if (window.visualViewport) {
42
+ window.visualViewport.removeEventListener("resize", setAppHeight);
43
+ window.visualViewport.removeEventListener("scroll", setAppHeight);
44
+ }
45
+ if ("orientation" in screen) {
46
+ screen.orientation.removeEventListener("change", setAppHeight);
47
+ }
48
+ };
49
+ }, []);
50
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Background operation runner for long-running tasks that shouldn't block UI
3
+ *
4
+ * Pattern:
5
+ * 1. Perform fast DB operations immediately
6
+ * 2. Return success to client
7
+ * 3. Run cleanup/slow operations in background
8
+ * 4. Log errors but don't fail the response
9
+ */
10
+
11
+ type BackgroundTask = () => Promise<void>;
12
+
13
+ /**
14
+ * Run a task in the background without blocking the response.
15
+ * Errors are logged but don't affect the caller.
16
+ */
17
+ export function runInBackground(task: BackgroundTask, taskName: string): void {
18
+ // Fire and forget - don't await
19
+ task().catch((error) => {
20
+ console.error(`[Background Task: ${taskName}] Error:`, error);
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Run multiple tasks in parallel in the background.
26
+ * All tasks run concurrently for speed.
27
+ */
28
+ export function runManyInBackground(
29
+ tasks: BackgroundTask[],
30
+ taskName: string
31
+ ): void {
32
+ Promise.all(tasks.map((task) => task())).catch((error) => {
33
+ console.error(`[Background Tasks: ${taskName}] Error:`, error);
34
+ });
35
+ }