@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.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/app/api/claude/hidden/route.ts +66 -0
- package/app/api/claude/projects/[name]/sessions/route.ts +71 -0
- package/app/api/claude/projects/route.ts +44 -0
- package/app/api/code-search/available/route.ts +12 -0
- package/app/api/code-search/route.ts +47 -0
- package/app/api/dev-servers/[id]/logs/route.ts +23 -0
- package/app/api/dev-servers/[id]/restart/route.ts +20 -0
- package/app/api/dev-servers/[id]/route.ts +51 -0
- package/app/api/dev-servers/[id]/stop/route.ts +20 -0
- package/app/api/dev-servers/detect/route.ts +39 -0
- package/app/api/dev-servers/route.ts +48 -0
- package/app/api/exec/route.ts +60 -0
- package/app/api/files/content/route.ts +76 -0
- package/app/api/files/route.ts +37 -0
- package/app/api/files/upload-temp/route.ts +41 -0
- package/app/api/git/check/route.ts +54 -0
- package/app/api/git/clone/route.ts +99 -0
- package/app/api/git/commit/route.ts +75 -0
- package/app/api/git/discard/route.ts +38 -0
- package/app/api/git/file-content/route.ts +64 -0
- package/app/api/git/history/[hash]/diff/route.ts +38 -0
- package/app/api/git/history/[hash]/route.ts +34 -0
- package/app/api/git/history/route.ts +27 -0
- package/app/api/git/multi-status/route.ts +46 -0
- package/app/api/git/pr/route.ts +164 -0
- package/app/api/git/push/route.ts +64 -0
- package/app/api/git/stage/route.ts +40 -0
- package/app/api/git/status/route.ts +51 -0
- package/app/api/git/unstage/route.ts +46 -0
- package/app/api/groups/[...path]/route.ts +136 -0
- package/app/api/groups/route.ts +93 -0
- package/app/api/orchestrate/spawn/route.ts +45 -0
- package/app/api/orchestrate/workers/[id]/route.ts +89 -0
- package/app/api/orchestrate/workers/route.ts +31 -0
- package/app/api/projects/[id]/detect/route.ts +27 -0
- package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +66 -0
- package/app/api/projects/[id]/dev-servers/route.ts +51 -0
- package/app/api/projects/[id]/repositories/[repoId]/route.ts +67 -0
- package/app/api/projects/[id]/repositories/route.ts +74 -0
- package/app/api/projects/[id]/route.ts +108 -0
- package/app/api/projects/detect/route.ts +33 -0
- package/app/api/projects/route.ts +59 -0
- package/app/api/sessions/[id]/claude-session/route.ts +42 -0
- package/app/api/sessions/[id]/fork/route.ts +74 -0
- package/app/api/sessions/[id]/mcp-config/route.ts +34 -0
- package/app/api/sessions/[id]/messages/route.ts +60 -0
- package/app/api/sessions/[id]/pr/route.ts +188 -0
- package/app/api/sessions/[id]/preview/route.ts +42 -0
- package/app/api/sessions/[id]/route.ts +229 -0
- package/app/api/sessions/[id]/send-keys/route.ts +119 -0
- package/app/api/sessions/[id]/summarize/route.ts +331 -0
- package/app/api/sessions/init-script/route.ts +84 -0
- package/app/api/sessions/route.ts +209 -0
- package/app/api/sessions/status/route.ts +237 -0
- package/app/api/system/route.ts +9 -0
- package/app/api/tmux/kill-all/route.ts +57 -0
- package/app/api/tmux/rename/route.ts +30 -0
- package/app/globals.css +174 -0
- package/app/icon.svg +11 -0
- package/app/layout.tsx +122 -0
- package/app/page.tsx +629 -0
- package/components/ChatMessage.tsx +65 -0
- package/components/ChatView.tsx +276 -0
- package/components/ClaudeProjects/ClaudeProjectCard.tsx +195 -0
- package/components/ClaudeProjects/ClaudeProjectsSection.tsx +89 -0
- package/components/ClaudeProjects/ClaudeSessionCard.tsx +100 -0
- package/components/ClaudeProjects/index.ts +1 -0
- package/components/CodeSearch/CodeSearchResults.tsx +177 -0
- package/components/ConductorPanel.tsx +256 -0
- package/components/DevServers/DevServerCard.tsx +311 -0
- package/components/DevServers/DevServersSection.tsx +91 -0
- package/components/DevServers/ServerLogsModal.tsx +151 -0
- package/components/DevServers/StartServerDialog.tsx +359 -0
- package/components/DevServers/index.ts +4 -0
- package/components/DiffViewer/DiffModal.tsx +151 -0
- package/components/DiffViewer/UnifiedDiff.tsx +185 -0
- package/components/DiffViewer/index.tsx +2 -0
- package/components/DirectoryPicker.tsx +355 -0
- package/components/FileExplorer/FileEditor.tsx +276 -0
- package/components/FileExplorer/FileTabs.tsx +118 -0
- package/components/FileExplorer/FileTree.tsx +214 -0
- package/components/FileExplorer/HtmlRenderer.tsx +16 -0
- package/components/FileExplorer/MarkdownRenderer.tsx +18 -0
- package/components/FileExplorer/index.tsx +520 -0
- package/components/FilePicker.tsx +339 -0
- package/components/FolderPicker.tsx +201 -0
- package/components/GitDrawer/FileEditDialog.tsx +400 -0
- package/components/GitDrawer/index.tsx +464 -0
- package/components/GitPanel/CommitForm.tsx +205 -0
- package/components/GitPanel/CommitHistory.tsx +174 -0
- package/components/GitPanel/CommitItem.tsx +196 -0
- package/components/GitPanel/FileChanges.tsx +414 -0
- package/components/GitPanel/GitPanelTabs.tsx +39 -0
- package/components/GitPanel/index.tsx +817 -0
- package/components/MessageInput.tsx +82 -0
- package/components/NewClaudeSessionDialog.tsx +166 -0
- package/components/NewSessionDialog/AdvancedSettings.tsx +78 -0
- package/components/NewSessionDialog/AgentSelector.tsx +37 -0
- package/components/NewSessionDialog/CreatingOverlay.tsx +94 -0
- package/components/NewSessionDialog/NewSessionDialog.types.ts +136 -0
- package/components/NewSessionDialog/ProjectSelector.tsx +146 -0
- package/components/NewSessionDialog/WorkingDirectoryInput.tsx +55 -0
- package/components/NewSessionDialog/WorktreeSection.tsx +92 -0
- package/components/NewSessionDialog/hooks/useNewSessionForm.ts +370 -0
- package/components/NewSessionDialog/index.tsx +106 -0
- package/components/NotificationSettings.tsx +127 -0
- package/components/PRCreationModal.tsx +272 -0
- package/components/Pane/DesktopTabBar.tsx +353 -0
- package/components/Pane/MobileTabBar.tsx +210 -0
- package/components/Pane/OpenInVSCode.tsx +69 -0
- package/components/Pane/PaneSkeletons.tsx +57 -0
- package/components/Pane/index.tsx +558 -0
- package/components/PaneLayout.tsx +60 -0
- package/components/Projects/DevServersSection.tsx +140 -0
- package/components/Projects/DirectoryField.tsx +92 -0
- package/components/Projects/NewProjectDialog.tsx +188 -0
- package/components/Projects/NewProjectDialog.types.ts +46 -0
- package/components/Projects/ProjectCard.tsx +276 -0
- package/components/Projects/ProjectSettingsDialog.tsx +811 -0
- package/components/Projects/hooks/useNewProjectForm.ts +249 -0
- package/components/Projects/index.ts +3 -0
- package/components/Providers.tsx +49 -0
- package/components/QuickSwitcher.tsx +306 -0
- package/components/SessionList/KillAllConfirm.tsx +46 -0
- package/components/SessionList/SelectionToolbar.tsx +164 -0
- package/components/SessionList/SessionList.types.ts +37 -0
- package/components/SessionList/SessionListHeader.tsx +71 -0
- package/components/SessionList/hooks/useSessionListMutations.ts +269 -0
- package/components/SessionList/index.tsx +189 -0
- package/components/ShellDrawer/index.tsx +106 -0
- package/components/SidebarFooter.tsx +55 -0
- package/components/Terminal/KeybarToggleButton.tsx +45 -0
- package/components/Terminal/ScrollToBottomButton.tsx +32 -0
- package/components/Terminal/SearchBar.tsx +71 -0
- package/components/Terminal/TerminalToolbar.tsx +551 -0
- package/components/Terminal/VirtualKeyboard.tsx +711 -0
- package/components/Terminal/constants.ts +20 -0
- package/components/Terminal/hooks/index.ts +5 -0
- package/components/Terminal/hooks/resize-handlers.ts +140 -0
- package/components/Terminal/hooks/terminal-init.ts +151 -0
- package/components/Terminal/hooks/touch-scroll.ts +155 -0
- package/components/Terminal/hooks/useTerminalConnection.ts +282 -0
- package/components/Terminal/hooks/useTerminalConnection.types.ts +39 -0
- package/components/Terminal/hooks/useTerminalSearch.ts +103 -0
- package/components/Terminal/hooks/websocket-connection.ts +274 -0
- package/components/Terminal/index.tsx +320 -0
- package/components/ThemeToggle.tsx +168 -0
- package/components/TmuxSessions.tsx +132 -0
- package/components/ToolCallDisplay.tsx +71 -0
- package/components/WorkerCard.tsx +245 -0
- package/components/a/ABadge.tsx +115 -0
- package/components/a/AButton.tsx +163 -0
- package/components/a/ADialog.tsx +93 -0
- package/components/a/ADropdownMenu.tsx +279 -0
- package/components/a/AIconButton.tsx +190 -0
- package/components/a/ASheet.tsx +150 -0
- package/components/a/ATooltip.tsx +77 -0
- package/components/a/index.ts +64 -0
- package/components/mobile/SwipeSidebar.tsx +122 -0
- package/components/ui/badge.tsx +41 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/context-menu.tsx +197 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/scroll-area.tsx +52 -0
- package/components/ui/select.tsx +159 -0
- package/components/ui/skeleton.tsx +111 -0
- package/components/ui/switch.tsx +31 -0
- package/components/ui/textarea.tsx +21 -0
- package/components/ui/tooltip.tsx +32 -0
- package/components/views/DesktopView.tsx +244 -0
- package/components/views/MobileView.tsx +110 -0
- package/components/views/types.ts +75 -0
- package/contexts/PaneContext.tsx +336 -0
- package/data/claude/index.ts +9 -0
- package/data/claude/keys.ts +6 -0
- package/data/claude/queries.ts +120 -0
- package/data/claude/useClaudeUpdates.ts +37 -0
- package/data/code-search/index.ts +2 -0
- package/data/code-search/keys.ts +7 -0
- package/data/code-search/queries.ts +61 -0
- package/data/dev-servers/index.ts +8 -0
- package/data/dev-servers/keys.ts +4 -0
- package/data/dev-servers/queries.ts +104 -0
- package/data/files/index.ts +3 -0
- package/data/files/keys.ts +4 -0
- package/data/files/queries.ts +25 -0
- package/data/git/keys.ts +15 -0
- package/data/git/queries.ts +395 -0
- package/data/groups/index.ts +1 -0
- package/data/groups/mutations.ts +95 -0
- package/data/projects/index.ts +10 -0
- package/data/projects/keys.ts +4 -0
- package/data/projects/queries.ts +193 -0
- package/data/repositories/index.ts +7 -0
- package/data/repositories/keys.ts +5 -0
- package/data/repositories/queries.ts +122 -0
- package/data/sessions/index.ts +12 -0
- package/data/sessions/keys.ts +8 -0
- package/data/sessions/queries.ts +218 -0
- package/data/statuses/index.ts +1 -0
- package/data/statuses/queries.ts +69 -0
- package/hooks/useCopyToClipboard.ts +48 -0
- package/hooks/useDevServersManager.ts +73 -0
- package/hooks/useDirectoryBrowser.ts +90 -0
- package/hooks/useDrawerAnimation.ts +27 -0
- package/hooks/useFileDrop.ts +87 -0
- package/hooks/useFileEditor.ts +184 -0
- package/hooks/useGroups.ts +37 -0
- package/hooks/useHomePath.ts +34 -0
- package/hooks/useKeyRepeat.ts +55 -0
- package/hooks/useKeybarVisibility.ts +42 -0
- package/hooks/useNotifications.ts +257 -0
- package/hooks/useProjects.ts +53 -0
- package/hooks/useSessionStatuses.ts +30 -0
- package/hooks/useSessions.ts +86 -0
- package/hooks/useSpeechRecognition.ts +124 -0
- package/hooks/useViewport.ts +32 -0
- package/hooks/useViewportHeight.ts +50 -0
- package/lib/async-operations.ts +35 -0
- package/lib/banner.ts +81 -0
- package/lib/claude/jsonl-cache.ts +86 -0
- package/lib/claude/jsonl-reader.ts +271 -0
- package/lib/claude/process-manager.ts +278 -0
- package/lib/claude/stream-parser.ts +173 -0
- package/lib/claude/types.ts +154 -0
- package/lib/claude/watcher.ts +71 -0
- package/lib/client/session-registry.ts +111 -0
- package/lib/code-search.ts +121 -0
- package/lib/db/index.ts +48 -0
- package/lib/db/migrations.ts +45 -0
- package/lib/db/queries.ts +460 -0
- package/lib/db/schema.ts +114 -0
- package/lib/db/types.ts +92 -0
- package/lib/db.ts +2 -0
- package/lib/dev-servers.ts +509 -0
- package/lib/diff-parser.ts +221 -0
- package/lib/env-setup.ts +285 -0
- package/lib/file-upload.ts +34 -0
- package/lib/file-utils.ts +50 -0
- package/lib/files.ts +207 -0
- package/lib/git-history.ts +294 -0
- package/lib/git-status.ts +391 -0
- package/lib/git.ts +257 -0
- package/lib/mcp-config.ts +81 -0
- package/lib/multi-repo-git.ts +179 -0
- package/lib/notifications.ts +219 -0
- package/lib/orchestration.ts +448 -0
- package/lib/panes.ts +232 -0
- package/lib/ports.ts +97 -0
- package/lib/pr-generation.ts +307 -0
- package/lib/pr.ts +234 -0
- package/lib/projects.ts +578 -0
- package/lib/providers/registry.ts +70 -0
- package/lib/providers.ts +121 -0
- package/lib/query-client.ts +14 -0
- package/lib/rangeSelectionUtils.ts +65 -0
- package/lib/status-detector.ts +375 -0
- package/lib/terminal-themes.ts +265 -0
- package/lib/theme-config.ts +327 -0
- package/lib/utils.ts +6 -0
- package/lib/worktrees.ts +262 -0
- package/mcp/orchestration-server.ts +438 -0
- package/package.json +139 -0
- package/postcss.config.mjs +7 -0
- package/public/icon.svg +10 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/manifest.json +61 -0
- package/public/sw.js +64 -0
- package/scripts/agent-os +91 -0
- package/scripts/install.sh +48 -0
- package/scripts/lib/ai-clis.sh +132 -0
- package/scripts/lib/commands.sh +487 -0
- package/scripts/lib/common.sh +89 -0
- package/scripts/lib/prerequisites.sh +462 -0
- package/scripts/setup.sh +134 -0
- package/server.ts +155 -0
- package/stores/fileOpen.ts +26 -0
- package/stores/index.ts +1 -0
- package/stores/initialPrompt.ts +24 -0
- package/stores/sessionSelection.ts +48 -0
- package/styles/themes.css +603 -0
- 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
|
+
}
|