@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,336 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useState,
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
import {
|
|
12
|
+
type PaneState,
|
|
13
|
+
type PaneData,
|
|
14
|
+
type TabData,
|
|
15
|
+
createInitialPaneState,
|
|
16
|
+
createPaneData,
|
|
17
|
+
createTab,
|
|
18
|
+
splitPane,
|
|
19
|
+
closePane,
|
|
20
|
+
countPanes,
|
|
21
|
+
savePaneState,
|
|
22
|
+
loadPaneState,
|
|
23
|
+
MAX_PANES,
|
|
24
|
+
} from "@/lib/panes";
|
|
25
|
+
import { useViewport } from "@/hooks/useViewport";
|
|
26
|
+
|
|
27
|
+
interface PaneContextValue {
|
|
28
|
+
state: PaneState;
|
|
29
|
+
focusedPaneId: string;
|
|
30
|
+
canSplit: boolean;
|
|
31
|
+
canClose: boolean;
|
|
32
|
+
isMobile: boolean;
|
|
33
|
+
focusPane: (paneId: string) => void;
|
|
34
|
+
splitHorizontal: (paneId: string) => void;
|
|
35
|
+
splitVertical: (paneId: string) => void;
|
|
36
|
+
close: (paneId: string) => void;
|
|
37
|
+
// Tab management
|
|
38
|
+
addTab: (paneId: string) => void;
|
|
39
|
+
closeTab: (paneId: string, tabId: string) => void;
|
|
40
|
+
switchTab: (paneId: string, tabId: string) => void;
|
|
41
|
+
// Session management (operates on active tab)
|
|
42
|
+
attachSession: (
|
|
43
|
+
paneId: string,
|
|
44
|
+
sessionId: string,
|
|
45
|
+
tmuxName: string,
|
|
46
|
+
sessionName?: string,
|
|
47
|
+
claudeProjectName?: string,
|
|
48
|
+
workingDirectory?: string
|
|
49
|
+
) => void;
|
|
50
|
+
detachSession: (paneId: string) => void;
|
|
51
|
+
reattachSession: (paneId: string) => void;
|
|
52
|
+
getPaneData: (paneId: string) => PaneData;
|
|
53
|
+
getActiveTab: (paneId: string) => TabData | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const PaneContext = createContext<PaneContextValue | null>(null);
|
|
57
|
+
|
|
58
|
+
// Default pane data for migration from old format
|
|
59
|
+
const defaultPaneData: PaneData = createPaneData();
|
|
60
|
+
|
|
61
|
+
export function PaneProvider({ children }: { children: ReactNode }) {
|
|
62
|
+
const [state, setState] = useState<PaneState>(createInitialPaneState);
|
|
63
|
+
const [hydrated, setHydrated] = useState(false);
|
|
64
|
+
const { isMobile } = useViewport();
|
|
65
|
+
|
|
66
|
+
// Load from localStorage after hydration
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const saved = loadPaneState();
|
|
69
|
+
if (saved) {
|
|
70
|
+
// Migrate old pane data format if needed
|
|
71
|
+
const migratedPanes: Record<string, PaneData> = {};
|
|
72
|
+
for (const [paneId, paneData] of Object.entries(saved.panes)) {
|
|
73
|
+
if ("tabs" in paneData && Array.isArray(paneData.tabs)) {
|
|
74
|
+
// New format
|
|
75
|
+
migratedPanes[paneId] = paneData as PaneData;
|
|
76
|
+
} else {
|
|
77
|
+
// Old format - migrate to new
|
|
78
|
+
const oldData = paneData as {
|
|
79
|
+
sessionId?: string | null;
|
|
80
|
+
attachedTmux?: string | null;
|
|
81
|
+
};
|
|
82
|
+
const tab = createTab();
|
|
83
|
+
tab.sessionId = oldData.sessionId || null;
|
|
84
|
+
tab.attachedTmux = oldData.attachedTmux || null;
|
|
85
|
+
migratedPanes[paneId] = {
|
|
86
|
+
tabs: [tab],
|
|
87
|
+
activeTabId: tab.id,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
setState({ ...saved, panes: migratedPanes });
|
|
92
|
+
}
|
|
93
|
+
setHydrated(true);
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
// Persist state changes to localStorage (only after hydration)
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (hydrated) {
|
|
99
|
+
savePaneState(state);
|
|
100
|
+
}
|
|
101
|
+
}, [state, hydrated]);
|
|
102
|
+
|
|
103
|
+
const focusPane = useCallback((paneId: string) => {
|
|
104
|
+
setState((prev) => ({ ...prev, focusedPaneId: paneId }));
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
const splitHorizontal = useCallback((paneId: string) => {
|
|
108
|
+
setState((prev) => {
|
|
109
|
+
const newState = splitPane(prev, paneId, "horizontal");
|
|
110
|
+
return newState || prev;
|
|
111
|
+
});
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
const splitVertical = useCallback((paneId: string) => {
|
|
115
|
+
setState((prev) => {
|
|
116
|
+
const newState = splitPane(prev, paneId, "vertical");
|
|
117
|
+
return newState || prev;
|
|
118
|
+
});
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
const close = useCallback((paneId: string) => {
|
|
122
|
+
setState((prev) => {
|
|
123
|
+
const newState = closePane(prev, paneId);
|
|
124
|
+
return newState || prev;
|
|
125
|
+
});
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
// Tab management
|
|
129
|
+
const addTab = useCallback((paneId: string) => {
|
|
130
|
+
setState((prev) => {
|
|
131
|
+
const pane = prev.panes[paneId];
|
|
132
|
+
if (!pane) return prev;
|
|
133
|
+
const newTab = createTab();
|
|
134
|
+
return {
|
|
135
|
+
...prev,
|
|
136
|
+
panes: {
|
|
137
|
+
...prev.panes,
|
|
138
|
+
[paneId]: {
|
|
139
|
+
...pane,
|
|
140
|
+
tabs: [...pane.tabs, newTab],
|
|
141
|
+
activeTabId: newTab.id,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
const closeTab = useCallback((paneId: string, tabId: string) => {
|
|
149
|
+
setState((prev) => {
|
|
150
|
+
const pane = prev.panes[paneId];
|
|
151
|
+
if (!pane || pane.tabs.length <= 1) return prev; // Keep at least one tab
|
|
152
|
+
|
|
153
|
+
const newTabs = pane.tabs.filter((t) => t.id !== tabId);
|
|
154
|
+
const newActiveTabId =
|
|
155
|
+
pane.activeTabId === tabId ? newTabs[0].id : pane.activeTabId;
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
...prev,
|
|
159
|
+
panes: {
|
|
160
|
+
...prev.panes,
|
|
161
|
+
[paneId]: {
|
|
162
|
+
...pane,
|
|
163
|
+
tabs: newTabs,
|
|
164
|
+
activeTabId: newActiveTabId,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
const switchTab = useCallback((paneId: string, tabId: string) => {
|
|
172
|
+
setState((prev) => {
|
|
173
|
+
const pane = prev.panes[paneId];
|
|
174
|
+
if (!pane) return prev;
|
|
175
|
+
return {
|
|
176
|
+
...prev,
|
|
177
|
+
panes: {
|
|
178
|
+
...prev.panes,
|
|
179
|
+
[paneId]: {
|
|
180
|
+
...pane,
|
|
181
|
+
activeTabId: tabId,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}, []);
|
|
187
|
+
|
|
188
|
+
// Attach session to active tab
|
|
189
|
+
const attachSession = useCallback(
|
|
190
|
+
(
|
|
191
|
+
paneId: string,
|
|
192
|
+
sessionId: string,
|
|
193
|
+
tmuxName: string,
|
|
194
|
+
sessionName?: string,
|
|
195
|
+
claudeProjectName?: string,
|
|
196
|
+
workingDirectory?: string
|
|
197
|
+
) => {
|
|
198
|
+
setState((prev) => {
|
|
199
|
+
const pane = prev.panes[paneId];
|
|
200
|
+
if (!pane) return prev;
|
|
201
|
+
|
|
202
|
+
const newTabs = pane.tabs.map((tab) =>
|
|
203
|
+
tab.id === pane.activeTabId
|
|
204
|
+
? {
|
|
205
|
+
...tab,
|
|
206
|
+
sessionId,
|
|
207
|
+
sessionName: sessionName ?? null,
|
|
208
|
+
claudeProjectName: claudeProjectName ?? null,
|
|
209
|
+
workingDirectory: workingDirectory ?? null,
|
|
210
|
+
attachedTmux: tmuxName,
|
|
211
|
+
}
|
|
212
|
+
: tab
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
...prev,
|
|
217
|
+
panes: {
|
|
218
|
+
...prev.panes,
|
|
219
|
+
[paneId]: { ...pane, tabs: newTabs },
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
[]
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Detach session from active tab (preserves reference for re-attach)
|
|
228
|
+
const detachSession = useCallback((paneId: string) => {
|
|
229
|
+
setState((prev) => {
|
|
230
|
+
const pane = prev.panes[paneId];
|
|
231
|
+
if (!pane) return prev;
|
|
232
|
+
|
|
233
|
+
const newTabs = pane.tabs.map((tab) =>
|
|
234
|
+
tab.id === pane.activeTabId
|
|
235
|
+
? {
|
|
236
|
+
...tab,
|
|
237
|
+
detachedTmux: tab.attachedTmux,
|
|
238
|
+
detachedSessionId: tab.sessionId,
|
|
239
|
+
sessionId: null,
|
|
240
|
+
attachedTmux: null,
|
|
241
|
+
}
|
|
242
|
+
: tab
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
...prev,
|
|
247
|
+
panes: {
|
|
248
|
+
...prev.panes,
|
|
249
|
+
[paneId]: { ...pane, tabs: newTabs },
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
}, []);
|
|
254
|
+
|
|
255
|
+
// Re-attach to the last detached session
|
|
256
|
+
const reattachSession = useCallback((paneId: string) => {
|
|
257
|
+
setState((prev) => {
|
|
258
|
+
const pane = prev.panes[paneId];
|
|
259
|
+
if (!pane) return prev;
|
|
260
|
+
|
|
261
|
+
const newTabs = pane.tabs.map((tab) =>
|
|
262
|
+
tab.id === pane.activeTabId && tab.detachedTmux
|
|
263
|
+
? {
|
|
264
|
+
...tab,
|
|
265
|
+
sessionId: tab.detachedSessionId,
|
|
266
|
+
attachedTmux: tab.detachedTmux,
|
|
267
|
+
detachedTmux: null,
|
|
268
|
+
detachedSessionId: null,
|
|
269
|
+
}
|
|
270
|
+
: tab
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
...prev,
|
|
275
|
+
panes: {
|
|
276
|
+
...prev.panes,
|
|
277
|
+
[paneId]: { ...pane, tabs: newTabs },
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
}, []);
|
|
282
|
+
|
|
283
|
+
const getPaneData = useCallback(
|
|
284
|
+
(paneId: string): PaneData => {
|
|
285
|
+
return state.panes[paneId] || defaultPaneData;
|
|
286
|
+
},
|
|
287
|
+
[state.panes]
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const getActiveTab = useCallback(
|
|
291
|
+
(paneId: string): TabData | null => {
|
|
292
|
+
const pane = state.panes[paneId];
|
|
293
|
+
if (!pane) return null;
|
|
294
|
+
return pane.tabs.find((t) => t.id === pane.activeTabId) || null;
|
|
295
|
+
},
|
|
296
|
+
[state.panes]
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// On mobile: disable splits (single pane only)
|
|
300
|
+
const canSplit = !isMobile && countPanes(state.layout) < MAX_PANES;
|
|
301
|
+
const canClose = !isMobile && countPanes(state.layout) > 1;
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<PaneContext.Provider
|
|
305
|
+
value={{
|
|
306
|
+
state,
|
|
307
|
+
focusedPaneId: state.focusedPaneId,
|
|
308
|
+
canSplit,
|
|
309
|
+
canClose,
|
|
310
|
+
isMobile,
|
|
311
|
+
focusPane,
|
|
312
|
+
splitHorizontal,
|
|
313
|
+
splitVertical,
|
|
314
|
+
close,
|
|
315
|
+
addTab,
|
|
316
|
+
closeTab,
|
|
317
|
+
switchTab,
|
|
318
|
+
attachSession,
|
|
319
|
+
detachSession,
|
|
320
|
+
reattachSession,
|
|
321
|
+
getPaneData,
|
|
322
|
+
getActiveTab,
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
{children}
|
|
326
|
+
</PaneContext.Provider>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function usePanes() {
|
|
331
|
+
const context = useContext(PaneContext);
|
|
332
|
+
if (!context) {
|
|
333
|
+
throw new Error("usePanes must be used within a PaneProvider");
|
|
334
|
+
}
|
|
335
|
+
return context;
|
|
336
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { claudeKeys } from "./keys";
|
|
2
|
+
export {
|
|
3
|
+
useClaudeProjectsQuery,
|
|
4
|
+
useClaudeSessionsQuery,
|
|
5
|
+
useHideItem,
|
|
6
|
+
useUnhideItem,
|
|
7
|
+
} from "./queries";
|
|
8
|
+
export type { ClaudeProject, ClaudeSession } from "./queries";
|
|
9
|
+
export { useClaudeUpdates } from "./useClaudeUpdates";
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { claudeKeys } from "./keys";
|
|
3
|
+
|
|
4
|
+
export interface ClaudeProject {
|
|
5
|
+
name: string;
|
|
6
|
+
directory: string;
|
|
7
|
+
displayName: string;
|
|
8
|
+
sessionCount: number;
|
|
9
|
+
lastActivity: string;
|
|
10
|
+
hidden: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ClaudeSession {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
summary: string;
|
|
16
|
+
lastActivity: string;
|
|
17
|
+
messageCount: number;
|
|
18
|
+
cwd: string | null;
|
|
19
|
+
hidden: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ClaudeSessionsResponse {
|
|
23
|
+
sessions: ClaudeSession[];
|
|
24
|
+
total: number;
|
|
25
|
+
hasMore: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function fetchClaudeProjects(): Promise<ClaudeProject[]> {
|
|
29
|
+
const res = await fetch("/api/claude/projects");
|
|
30
|
+
if (!res.ok) throw new Error("Failed to fetch Claude projects");
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
return data.projects || [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function fetchClaudeSessions(
|
|
36
|
+
projectName: string,
|
|
37
|
+
limit = 50,
|
|
38
|
+
offset = 0,
|
|
39
|
+
includeHidden = true
|
|
40
|
+
): Promise<ClaudeSessionsResponse> {
|
|
41
|
+
const params = new URLSearchParams({
|
|
42
|
+
limit: String(limit),
|
|
43
|
+
offset: String(offset),
|
|
44
|
+
includeHidden: String(includeHidden),
|
|
45
|
+
});
|
|
46
|
+
const res = await fetch(
|
|
47
|
+
`/api/claude/projects/${encodeURIComponent(projectName)}/sessions?${params}`
|
|
48
|
+
);
|
|
49
|
+
if (!res.ok) throw new Error("Failed to fetch Claude sessions");
|
|
50
|
+
return res.json();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useClaudeProjectsQuery() {
|
|
54
|
+
return useQuery({
|
|
55
|
+
queryKey: claudeKeys.projects(),
|
|
56
|
+
queryFn: fetchClaudeProjects,
|
|
57
|
+
staleTime: 30000,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function useClaudeSessionsQuery(projectName: string | null) {
|
|
62
|
+
return useQuery({
|
|
63
|
+
queryKey: claudeKeys.sessions(projectName || ""),
|
|
64
|
+
queryFn: () => fetchClaudeSessions(projectName!),
|
|
65
|
+
enabled: !!projectName,
|
|
66
|
+
staleTime: 30000,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function useHideItem() {
|
|
71
|
+
const queryClient = useQueryClient();
|
|
72
|
+
|
|
73
|
+
return useMutation({
|
|
74
|
+
mutationFn: async ({
|
|
75
|
+
itemType,
|
|
76
|
+
itemId,
|
|
77
|
+
}: {
|
|
78
|
+
itemType: "project" | "session";
|
|
79
|
+
itemId: string;
|
|
80
|
+
}) => {
|
|
81
|
+
const res = await fetch("/api/claude/hidden", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "Content-Type": "application/json" },
|
|
84
|
+
body: JSON.stringify({ itemType, itemId }),
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) throw new Error("Failed to hide item");
|
|
87
|
+
return res.json();
|
|
88
|
+
},
|
|
89
|
+
onSuccess: () => {
|
|
90
|
+
queryClient.invalidateQueries({ queryKey: claudeKeys.projects() });
|
|
91
|
+
queryClient.invalidateQueries({ queryKey: claudeKeys.all });
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function useUnhideItem() {
|
|
97
|
+
const queryClient = useQueryClient();
|
|
98
|
+
|
|
99
|
+
return useMutation({
|
|
100
|
+
mutationFn: async ({
|
|
101
|
+
itemType,
|
|
102
|
+
itemId,
|
|
103
|
+
}: {
|
|
104
|
+
itemType: "project" | "session";
|
|
105
|
+
itemId: string;
|
|
106
|
+
}) => {
|
|
107
|
+
const res = await fetch("/api/claude/hidden", {
|
|
108
|
+
method: "DELETE",
|
|
109
|
+
headers: { "Content-Type": "application/json" },
|
|
110
|
+
body: JSON.stringify({ itemType, itemId }),
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) throw new Error("Failed to unhide item");
|
|
113
|
+
return res.json();
|
|
114
|
+
},
|
|
115
|
+
onSuccess: () => {
|
|
116
|
+
queryClient.invalidateQueries({ queryKey: claudeKeys.projects() });
|
|
117
|
+
queryClient.invalidateQueries({ queryKey: claudeKeys.all });
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { claudeKeys } from "./keys";
|
|
6
|
+
|
|
7
|
+
export function useClaudeUpdates() {
|
|
8
|
+
const queryClient = useQueryClient();
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
12
|
+
const ws = new WebSocket(
|
|
13
|
+
`${protocol}//${window.location.host}/ws/updates`
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
ws.onmessage = (event) => {
|
|
17
|
+
try {
|
|
18
|
+
const msg = JSON.parse(event.data);
|
|
19
|
+
if (msg.type === "project-updated") {
|
|
20
|
+
queryClient.refetchQueries({
|
|
21
|
+
queryKey: claudeKeys.sessions(msg.projectName),
|
|
22
|
+
});
|
|
23
|
+
queryClient.refetchQueries({
|
|
24
|
+
queryKey: claudeKeys.projects(),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (msg.type === "projects-changed") {
|
|
28
|
+
queryClient.refetchQueries({
|
|
29
|
+
queryKey: claudeKeys.projects(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
} catch {}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return () => ws.close();
|
|
36
|
+
}, [queryClient]);
|
|
37
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const codeSearchKeys = {
|
|
2
|
+
all: ["code-search"] as const,
|
|
3
|
+
available: () => [...codeSearchKeys.all, "available"] as const,
|
|
4
|
+
searches: () => [...codeSearchKeys.all, "searches"] as const,
|
|
5
|
+
search: (path: string, query: string) =>
|
|
6
|
+
[...codeSearchKeys.searches(), path, query] as const,
|
|
7
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { codeSearchKeys } from "./keys";
|
|
3
|
+
import type { FormattedMatch } from "@/lib/code-search";
|
|
4
|
+
|
|
5
|
+
interface AvailabilityResponse {
|
|
6
|
+
available: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface SearchResponse {
|
|
10
|
+
results: FormattedMatch[];
|
|
11
|
+
query: string;
|
|
12
|
+
path: string;
|
|
13
|
+
count: number;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function fetchRipgrepAvailability(): Promise<boolean> {
|
|
18
|
+
const res = await fetch("/api/code-search/available");
|
|
19
|
+
const data: AvailabilityResponse = await res.json();
|
|
20
|
+
return data.available;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function fetchCodeSearch(
|
|
24
|
+
path: string,
|
|
25
|
+
query: string
|
|
26
|
+
): Promise<SearchResponse> {
|
|
27
|
+
const params = new URLSearchParams({
|
|
28
|
+
path,
|
|
29
|
+
query,
|
|
30
|
+
maxResults: "100",
|
|
31
|
+
contextLines: "2",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const res = await fetch(`/api/code-search?${params}`);
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
throw new Error(data.error || "Failed to search code");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useRipgrepAvailable() {
|
|
45
|
+
return useQuery({
|
|
46
|
+
queryKey: codeSearchKeys.available(),
|
|
47
|
+
queryFn: fetchRipgrepAvailability,
|
|
48
|
+
staleTime: Infinity, // Never refetch - ripgrep installation doesn't change during runtime
|
|
49
|
+
gcTime: Infinity,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useCodeSearch(path: string, query: string, enabled = false) {
|
|
54
|
+
return useQuery({
|
|
55
|
+
queryKey: codeSearchKeys.search(path, query),
|
|
56
|
+
queryFn: () => fetchCodeSearch(path, query),
|
|
57
|
+
enabled: enabled && query.length > 2,
|
|
58
|
+
staleTime: 30000,
|
|
59
|
+
gcTime: 60000,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import type { DevServer } from "@/lib/db";
|
|
3
|
+
import { devServerKeys } from "./keys";
|
|
4
|
+
|
|
5
|
+
async function fetchDevServers(): Promise<DevServer[]> {
|
|
6
|
+
const res = await fetch("/api/dev-servers");
|
|
7
|
+
if (!res.ok) throw new Error("Failed to fetch dev servers");
|
|
8
|
+
const data = await res.json();
|
|
9
|
+
return data.servers || [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useDevServersQuery() {
|
|
13
|
+
return useQuery({
|
|
14
|
+
queryKey: devServerKeys.list(),
|
|
15
|
+
queryFn: fetchDevServers,
|
|
16
|
+
staleTime: 3000,
|
|
17
|
+
refetchInterval: (query) => {
|
|
18
|
+
const servers = query.state.data;
|
|
19
|
+
if (!servers?.length) return false;
|
|
20
|
+
|
|
21
|
+
const hasRunning = servers.some((s) => s.status === "running");
|
|
22
|
+
return hasRunning ? 5000 : 30000;
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useStopDevServer() {
|
|
28
|
+
const queryClient = useQueryClient();
|
|
29
|
+
|
|
30
|
+
return useMutation({
|
|
31
|
+
mutationFn: async (serverId: string) => {
|
|
32
|
+
const res = await fetch(`/api/dev-servers/${serverId}/stop`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) throw new Error("Failed to stop dev server");
|
|
36
|
+
return res.json();
|
|
37
|
+
},
|
|
38
|
+
onSuccess: () => {
|
|
39
|
+
queryClient.invalidateQueries({ queryKey: devServerKeys.list() });
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useRestartDevServer() {
|
|
45
|
+
const queryClient = useQueryClient();
|
|
46
|
+
|
|
47
|
+
return useMutation({
|
|
48
|
+
mutationFn: async (serverId: string) => {
|
|
49
|
+
const res = await fetch(`/api/dev-servers/${serverId}/restart`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) throw new Error("Failed to restart dev server");
|
|
53
|
+
return res.json();
|
|
54
|
+
},
|
|
55
|
+
onSuccess: () => {
|
|
56
|
+
queryClient.invalidateQueries({ queryKey: devServerKeys.list() });
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function useRemoveDevServer() {
|
|
62
|
+
const queryClient = useQueryClient();
|
|
63
|
+
|
|
64
|
+
return useMutation({
|
|
65
|
+
mutationFn: async (serverId: string) => {
|
|
66
|
+
const res = await fetch(`/api/dev-servers/${serverId}`, {
|
|
67
|
+
method: "DELETE",
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) throw new Error("Failed to remove dev server");
|
|
70
|
+
return res.json();
|
|
71
|
+
},
|
|
72
|
+
onSuccess: () => {
|
|
73
|
+
queryClient.invalidateQueries({ queryKey: devServerKeys.list() });
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface CreateDevServerOptions {
|
|
79
|
+
projectId: string;
|
|
80
|
+
type: "node" | "docker";
|
|
81
|
+
name: string;
|
|
82
|
+
command: string;
|
|
83
|
+
workingDirectory: string;
|
|
84
|
+
ports?: number[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function useCreateDevServer() {
|
|
88
|
+
const queryClient = useQueryClient();
|
|
89
|
+
|
|
90
|
+
return useMutation({
|
|
91
|
+
mutationFn: async (opts: CreateDevServerOptions) => {
|
|
92
|
+
const res = await fetch("/api/dev-servers", {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "Content-Type": "application/json" },
|
|
95
|
+
body: JSON.stringify(opts),
|
|
96
|
+
});
|
|
97
|
+
if (!res.ok) throw new Error("Failed to create dev server");
|
|
98
|
+
return res.json();
|
|
99
|
+
},
|
|
100
|
+
onSuccess: () => {
|
|
101
|
+
queryClient.invalidateQueries({ queryKey: devServerKeys.list() });
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|