@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
package/lib/panes.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Multi-pane layout types and helpers
|
|
2
|
+
|
|
3
|
+
export type PaneLayout = PaneLayoutLeaf | PaneLayoutSplit;
|
|
4
|
+
|
|
5
|
+
export interface PaneLayoutLeaf {
|
|
6
|
+
type: "leaf";
|
|
7
|
+
paneId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PaneLayoutSplit {
|
|
11
|
+
type: "split";
|
|
12
|
+
direction: "horizontal" | "vertical";
|
|
13
|
+
children: PaneLayout[];
|
|
14
|
+
sizes: number[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TabData {
|
|
18
|
+
id: string;
|
|
19
|
+
sessionId: string | null;
|
|
20
|
+
sessionName: string | null;
|
|
21
|
+
claudeProjectName: string | null;
|
|
22
|
+
workingDirectory: string | null;
|
|
23
|
+
attachedTmux: string | null;
|
|
24
|
+
detachedTmux: string | null;
|
|
25
|
+
detachedSessionId: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PaneData {
|
|
29
|
+
tabs: TabData[];
|
|
30
|
+
activeTabId: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PaneState {
|
|
34
|
+
layout: PaneLayout;
|
|
35
|
+
focusedPaneId: string;
|
|
36
|
+
panes: Record<string, PaneData>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Generate unique tab ID
|
|
40
|
+
export function generateTabId(): string {
|
|
41
|
+
return `tab-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Create a new tab
|
|
45
|
+
export function createTab(): TabData {
|
|
46
|
+
return {
|
|
47
|
+
id: generateTabId(),
|
|
48
|
+
sessionId: null,
|
|
49
|
+
sessionName: null,
|
|
50
|
+
claudeProjectName: null,
|
|
51
|
+
workingDirectory: null,
|
|
52
|
+
attachedTmux: null,
|
|
53
|
+
detachedTmux: null,
|
|
54
|
+
detachedSessionId: null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create initial pane data with one tab
|
|
59
|
+
export function createPaneData(): PaneData {
|
|
60
|
+
const tab = createTab();
|
|
61
|
+
return {
|
|
62
|
+
tabs: [tab],
|
|
63
|
+
activeTabId: tab.id,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const MAX_PANES = 4;
|
|
68
|
+
|
|
69
|
+
// Generate unique pane ID
|
|
70
|
+
export function generatePaneId(): string {
|
|
71
|
+
return `pane-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Create initial state with single pane
|
|
75
|
+
export function createInitialPaneState(): PaneState {
|
|
76
|
+
const paneId = generatePaneId();
|
|
77
|
+
return {
|
|
78
|
+
layout: { type: "leaf", paneId },
|
|
79
|
+
focusedPaneId: paneId,
|
|
80
|
+
panes: {
|
|
81
|
+
[paneId]: createPaneData(),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Count total panes in layout
|
|
87
|
+
export function countPanes(layout: PaneLayout): number {
|
|
88
|
+
if (layout.type === "leaf") {
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
return layout.children.reduce((sum, child) => sum + countPanes(child), 0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Find and replace a pane in the layout
|
|
95
|
+
export function replacePane(
|
|
96
|
+
layout: PaneLayout,
|
|
97
|
+
targetPaneId: string,
|
|
98
|
+
newLayout: PaneLayout
|
|
99
|
+
): PaneLayout {
|
|
100
|
+
if (layout.type === "leaf") {
|
|
101
|
+
return layout.paneId === targetPaneId ? newLayout : layout;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
...layout,
|
|
105
|
+
children: layout.children.map((child) =>
|
|
106
|
+
replacePane(child, targetPaneId, newLayout)
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Split a pane
|
|
112
|
+
export function splitPane(
|
|
113
|
+
state: PaneState,
|
|
114
|
+
paneId: string,
|
|
115
|
+
direction: "horizontal" | "vertical"
|
|
116
|
+
): PaneState | null {
|
|
117
|
+
if (countPanes(state.layout) >= MAX_PANES) {
|
|
118
|
+
return null; // Max panes reached
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const newPaneId = generatePaneId();
|
|
122
|
+
const newSplit: PaneLayoutSplit = {
|
|
123
|
+
type: "split",
|
|
124
|
+
direction,
|
|
125
|
+
children: [
|
|
126
|
+
{ type: "leaf", paneId },
|
|
127
|
+
{ type: "leaf", paneId: newPaneId },
|
|
128
|
+
],
|
|
129
|
+
sizes: [50, 50],
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
layout: replacePane(state.layout, paneId, newSplit),
|
|
134
|
+
focusedPaneId: newPaneId,
|
|
135
|
+
panes: {
|
|
136
|
+
...state.panes,
|
|
137
|
+
[newPaneId]: createPaneData(),
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Remove a pane from layout and return the remaining layout
|
|
143
|
+
function removePaneFromLayout(
|
|
144
|
+
layout: PaneLayout,
|
|
145
|
+
paneId: string
|
|
146
|
+
): PaneLayout | null {
|
|
147
|
+
if (layout.type === "leaf") {
|
|
148
|
+
return layout.paneId === paneId ? null : layout;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const newChildren: PaneLayout[] = [];
|
|
152
|
+
for (const child of layout.children) {
|
|
153
|
+
const result = removePaneFromLayout(child, paneId);
|
|
154
|
+
if (result !== null) {
|
|
155
|
+
newChildren.push(result);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (newChildren.length === 0) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
if (newChildren.length === 1) {
|
|
163
|
+
return newChildren[0]; // Collapse single-child splits
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Redistribute sizes
|
|
167
|
+
const totalSize = layout.sizes.reduce((a, b) => a + b, 0);
|
|
168
|
+
const newSizes = newChildren.map(() => totalSize / newChildren.length);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
...layout,
|
|
172
|
+
children: newChildren,
|
|
173
|
+
sizes: newSizes,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Close a pane
|
|
178
|
+
export function closePane(state: PaneState, paneId: string): PaneState | null {
|
|
179
|
+
if (countPanes(state.layout) <= 1) {
|
|
180
|
+
return null; // Can't close last pane
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const newLayout = removePaneFromLayout(state.layout, paneId);
|
|
184
|
+
if (!newLayout) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { [paneId]: _, ...remainingPanes } = state.panes;
|
|
189
|
+
|
|
190
|
+
// If focused pane was closed, focus first remaining pane
|
|
191
|
+
let newFocusedId = state.focusedPaneId;
|
|
192
|
+
if (paneId === state.focusedPaneId) {
|
|
193
|
+
newFocusedId = Object.keys(remainingPanes)[0];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
layout: newLayout,
|
|
198
|
+
focusedPaneId: newFocusedId,
|
|
199
|
+
panes: remainingPanes,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Get all pane IDs from layout
|
|
204
|
+
export function getAllPaneIds(layout: PaneLayout): string[] {
|
|
205
|
+
if (layout.type === "leaf") {
|
|
206
|
+
return [layout.paneId];
|
|
207
|
+
}
|
|
208
|
+
return layout.children.flatMap(getAllPaneIds);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// localStorage key for persisting pane state
|
|
212
|
+
const PANE_STATE_KEY = "claude-deck-pane-state";
|
|
213
|
+
|
|
214
|
+
export function savePaneState(state: PaneState): void {
|
|
215
|
+
try {
|
|
216
|
+
localStorage.setItem(PANE_STATE_KEY, JSON.stringify(state));
|
|
217
|
+
} catch {
|
|
218
|
+
// localStorage might be unavailable
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function loadPaneState(): PaneState | null {
|
|
223
|
+
try {
|
|
224
|
+
const saved = localStorage.getItem(PANE_STATE_KEY);
|
|
225
|
+
if (saved) {
|
|
226
|
+
return JSON.parse(saved);
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
// localStorage might be unavailable or data corrupt
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
package/lib/ports.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port Management for Dev Servers
|
|
3
|
+
*
|
|
4
|
+
* Assigns unique ports to worktree sessions to avoid conflicts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { exec } from "child_process";
|
|
8
|
+
import { promisify } from "util";
|
|
9
|
+
import { getDb } from "./db";
|
|
10
|
+
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
|
|
13
|
+
// Port range for dev servers
|
|
14
|
+
const BASE_PORT = 3100;
|
|
15
|
+
const PORT_INCREMENT = 10;
|
|
16
|
+
const MAX_PORT = 3900;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if a port is in use
|
|
20
|
+
*/
|
|
21
|
+
export async function isPortInUse(port: number): Promise<boolean> {
|
|
22
|
+
try {
|
|
23
|
+
const { stdout } = await execAsync(
|
|
24
|
+
`lsof -i :${port} -sTCP:LISTEN 2>/dev/null | head -1`,
|
|
25
|
+
{ timeout: 5000 }
|
|
26
|
+
);
|
|
27
|
+
return stdout.trim().length > 0;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get all ports currently assigned to sessions
|
|
35
|
+
*/
|
|
36
|
+
export async function getAssignedPorts(): Promise<number[]> {
|
|
37
|
+
const rows = getDb()
|
|
38
|
+
.prepare(
|
|
39
|
+
"SELECT dev_server_port FROM sessions WHERE dev_server_port IS NOT NULL"
|
|
40
|
+
)
|
|
41
|
+
.all() as Array<{ dev_server_port: number }>;
|
|
42
|
+
return rows.map((s) => s.dev_server_port);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Find the next available port
|
|
47
|
+
*/
|
|
48
|
+
export async function findAvailablePort(): Promise<number> {
|
|
49
|
+
const assignedPorts = new Set(await getAssignedPorts());
|
|
50
|
+
|
|
51
|
+
for (let port = BASE_PORT; port <= MAX_PORT; port += PORT_INCREMENT) {
|
|
52
|
+
// Skip if already assigned to a session
|
|
53
|
+
if (assignedPorts.has(port)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check if port is actually in use (by something outside ClaudeDeck)
|
|
58
|
+
if (!(await isPortInUse(port))) {
|
|
59
|
+
return port;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Fallback: return a random port in range
|
|
64
|
+
return BASE_PORT + Math.floor(Math.random() * 80) * PORT_INCREMENT;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Assign a port to a session
|
|
69
|
+
*/
|
|
70
|
+
export async function assignPort(sessionId: string): Promise<number> {
|
|
71
|
+
const port = await findAvailablePort();
|
|
72
|
+
getDb()
|
|
73
|
+
.prepare("UPDATE sessions SET dev_server_port = ? WHERE id = ?")
|
|
74
|
+
.run(port, sessionId);
|
|
75
|
+
return port;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Release a port from a session
|
|
80
|
+
*/
|
|
81
|
+
export async function releasePort(sessionId: string): Promise<void> {
|
|
82
|
+
getDb()
|
|
83
|
+
.prepare("UPDATE sessions SET dev_server_port = NULL WHERE id = ?")
|
|
84
|
+
.run(sessionId);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the port assigned to a session
|
|
89
|
+
*/
|
|
90
|
+
export async function getSessionPort(
|
|
91
|
+
sessionId: string
|
|
92
|
+
): Promise<number | null> {
|
|
93
|
+
const result = getDb()
|
|
94
|
+
.prepare("SELECT dev_server_port FROM sessions WHERE id = ?")
|
|
95
|
+
.get(sessionId) as { dev_server_port: number | null } | undefined;
|
|
96
|
+
return result?.dev_server_port || null;
|
|
97
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
|
|
3
|
+
export interface GeneratedPRContent {
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate PR title and description using Claude CLI or fallback heuristics
|
|
10
|
+
*/
|
|
11
|
+
export async function generatePRContent(
|
|
12
|
+
workingDir: string,
|
|
13
|
+
baseBranch: string = "main"
|
|
14
|
+
): Promise<GeneratedPRContent> {
|
|
15
|
+
try {
|
|
16
|
+
// Get git context
|
|
17
|
+
const { diff, commits, changedFiles } = getGitContext(
|
|
18
|
+
workingDir,
|
|
19
|
+
baseBranch
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (!diff && commits.length === 0) {
|
|
23
|
+
return generateFallbackContent(changedFiles);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Try Claude CLI first
|
|
27
|
+
try {
|
|
28
|
+
const result = await generateWithClaude(workingDir, diff, commits);
|
|
29
|
+
if (result) {
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.debug("Claude CLI generation failed, using fallback", error);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fallback to heuristic generation
|
|
37
|
+
return generateHeuristicContent(diff, commits, changedFiles);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error("Failed to generate PR content", error);
|
|
40
|
+
return generateFallbackContent([]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get git context for PR generation
|
|
46
|
+
*/
|
|
47
|
+
function getGitContext(
|
|
48
|
+
workingDir: string,
|
|
49
|
+
baseBranch: string
|
|
50
|
+
): { diff: string; commits: string[]; changedFiles: string[] } {
|
|
51
|
+
let diff = "";
|
|
52
|
+
let commits: string[] = [];
|
|
53
|
+
let changedFiles: string[] = [];
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Try to get the remote base branch reference
|
|
57
|
+
let baseBranchRef = baseBranch;
|
|
58
|
+
try {
|
|
59
|
+
execSync(`git rev-parse --verify origin/${baseBranch}`, {
|
|
60
|
+
cwd: workingDir,
|
|
61
|
+
stdio: "pipe",
|
|
62
|
+
});
|
|
63
|
+
baseBranchRef = `origin/${baseBranch}`;
|
|
64
|
+
} catch {
|
|
65
|
+
// Fall back to local branch
|
|
66
|
+
try {
|
|
67
|
+
execSync(`git rev-parse --verify ${baseBranch}`, {
|
|
68
|
+
cwd: workingDir,
|
|
69
|
+
stdio: "pipe",
|
|
70
|
+
});
|
|
71
|
+
} catch {
|
|
72
|
+
// Base branch doesn't exist
|
|
73
|
+
return { diff, commits, changedFiles };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Get diff stats
|
|
78
|
+
try {
|
|
79
|
+
diff = execSync(`git diff ${baseBranchRef}...HEAD --stat`, {
|
|
80
|
+
cwd: workingDir,
|
|
81
|
+
encoding: "utf-8",
|
|
82
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
83
|
+
});
|
|
84
|
+
} catch {}
|
|
85
|
+
|
|
86
|
+
// Get changed files
|
|
87
|
+
try {
|
|
88
|
+
const filesOut = execSync(
|
|
89
|
+
`git diff --name-only ${baseBranchRef}...HEAD`,
|
|
90
|
+
{
|
|
91
|
+
cwd: workingDir,
|
|
92
|
+
encoding: "utf-8",
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
changedFiles = filesOut
|
|
96
|
+
.split("\n")
|
|
97
|
+
.map((f) => f.trim())
|
|
98
|
+
.filter(Boolean);
|
|
99
|
+
} catch {}
|
|
100
|
+
|
|
101
|
+
// Get commit messages
|
|
102
|
+
try {
|
|
103
|
+
const commitsOut = execSync(
|
|
104
|
+
`git log ${baseBranchRef}..HEAD --pretty=format:"%s"`,
|
|
105
|
+
{
|
|
106
|
+
cwd: workingDir,
|
|
107
|
+
encoding: "utf-8",
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
commits = commitsOut
|
|
111
|
+
.split("\n")
|
|
112
|
+
.map((c) => c.trim())
|
|
113
|
+
.filter(Boolean);
|
|
114
|
+
} catch {}
|
|
115
|
+
|
|
116
|
+
// Also include uncommitted changes
|
|
117
|
+
try {
|
|
118
|
+
const workingDiff = execSync("git diff --stat", {
|
|
119
|
+
cwd: workingDir,
|
|
120
|
+
encoding: "utf-8",
|
|
121
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
122
|
+
});
|
|
123
|
+
if (workingDiff) {
|
|
124
|
+
diff = diff ? `${diff}\n${workingDiff}` : workingDiff;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const uncommittedFiles = execSync("git diff --name-only", {
|
|
128
|
+
cwd: workingDir,
|
|
129
|
+
encoding: "utf-8",
|
|
130
|
+
})
|
|
131
|
+
.split("\n")
|
|
132
|
+
.map((f) => f.trim())
|
|
133
|
+
.filter(Boolean);
|
|
134
|
+
|
|
135
|
+
changedFiles = [...new Set([...changedFiles, ...uncommittedFiles])];
|
|
136
|
+
} catch {}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.warn("Failed to get git context", error);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { diff, commits, changedFiles };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generate PR content using Claude CLI
|
|
146
|
+
*/
|
|
147
|
+
async function generateWithClaude(
|
|
148
|
+
workingDir: string,
|
|
149
|
+
diff: string,
|
|
150
|
+
commits: string[]
|
|
151
|
+
): Promise<GeneratedPRContent | null> {
|
|
152
|
+
// Check if Claude CLI is available
|
|
153
|
+
try {
|
|
154
|
+
execSync("claude --version", { stdio: "pipe", timeout: 5000 });
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const prompt = buildPRPrompt(diff, commits);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Use claude CLI with --print flag for non-interactive output
|
|
163
|
+
const output = execSync(`claude --print "${prompt.replace(/"/g, '\\"')}"`, {
|
|
164
|
+
cwd: workingDir,
|
|
165
|
+
encoding: "utf-8",
|
|
166
|
+
timeout: 30000,
|
|
167
|
+
maxBuffer: 1024 * 1024,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return parseClaudeResponse(output);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.debug("Claude CLI invocation failed", error);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Build prompt for PR generation
|
|
179
|
+
*/
|
|
180
|
+
function buildPRPrompt(diff: string, commits: string[]): string {
|
|
181
|
+
const commitContext =
|
|
182
|
+
commits.length > 0
|
|
183
|
+
? `Commits:\n${commits.map((c) => `- ${c}`).join("\n")}`
|
|
184
|
+
: "";
|
|
185
|
+
const diffContext = diff
|
|
186
|
+
? `Diff summary:\n${diff.substring(0, 2000)}${diff.length > 2000 ? "..." : ""}`
|
|
187
|
+
: "";
|
|
188
|
+
|
|
189
|
+
return `Generate a concise PR title and description based on these changes:
|
|
190
|
+
|
|
191
|
+
${commitContext}
|
|
192
|
+
|
|
193
|
+
${diffContext}
|
|
194
|
+
|
|
195
|
+
Respond ONLY with valid JSON in this exact format:
|
|
196
|
+
{"title": "A concise PR title (max 72 chars)", "description": "A markdown description with ## headers and - bullet points"}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Parse Claude response into PR content
|
|
201
|
+
*/
|
|
202
|
+
function parseClaudeResponse(response: string): GeneratedPRContent | null {
|
|
203
|
+
try {
|
|
204
|
+
// Extract JSON from response
|
|
205
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
206
|
+
if (jsonMatch) {
|
|
207
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
208
|
+
if (parsed.title && parsed.description) {
|
|
209
|
+
let description = String(parsed.description);
|
|
210
|
+
// Handle escaped newlines
|
|
211
|
+
description = description.replace(/\\n/g, "\n");
|
|
212
|
+
description = description.replace(/\\\\n/g, "\n");
|
|
213
|
+
return {
|
|
214
|
+
title: parsed.title.trim(),
|
|
215
|
+
description: description.trim(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.debug("Failed to parse Claude response", error);
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Generate PR content using heuristics
|
|
227
|
+
*/
|
|
228
|
+
function generateHeuristicContent(
|
|
229
|
+
diff: string,
|
|
230
|
+
commits: string[],
|
|
231
|
+
changedFiles: string[]
|
|
232
|
+
): GeneratedPRContent {
|
|
233
|
+
// Use first commit as title
|
|
234
|
+
let title = "chore: update code";
|
|
235
|
+
if (commits.length > 0) {
|
|
236
|
+
title = commits[0];
|
|
237
|
+
if (title.length > 72) {
|
|
238
|
+
title = title.substring(0, 69) + "...";
|
|
239
|
+
}
|
|
240
|
+
} else if (changedFiles.length > 0) {
|
|
241
|
+
const fileName = changedFiles[0].split("/").pop() || "files";
|
|
242
|
+
title = `chore: update ${fileName}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Build description
|
|
246
|
+
const parts: string[] = [];
|
|
247
|
+
|
|
248
|
+
if (commits.length > 0) {
|
|
249
|
+
parts.push("## Changes\n");
|
|
250
|
+
commits.forEach((commit) => parts.push(`- ${commit}`));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (changedFiles.length > 0) {
|
|
254
|
+
parts.push("\n## Files Changed\n");
|
|
255
|
+
changedFiles.slice(0, 15).forEach((file) => parts.push(`- \`${file}\``));
|
|
256
|
+
if (changedFiles.length > 15) {
|
|
257
|
+
parts.push(`\n... and ${changedFiles.length - 15} more files`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Parse diff stats
|
|
262
|
+
if (diff) {
|
|
263
|
+
const statsMatch = diff.match(
|
|
264
|
+
/(\d+)\s+files? changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/
|
|
265
|
+
);
|
|
266
|
+
if (statsMatch) {
|
|
267
|
+
const fileCount = parseInt(statsMatch[1] || "0", 10);
|
|
268
|
+
const insertions = parseInt(statsMatch[2] || "0", 10);
|
|
269
|
+
const deletions = parseInt(statsMatch[3] || "0", 10);
|
|
270
|
+
|
|
271
|
+
if (fileCount > 0 || insertions > 0 || deletions > 0) {
|
|
272
|
+
parts.push("\n## Summary\n");
|
|
273
|
+
if (fileCount > 0) {
|
|
274
|
+
parts.push(
|
|
275
|
+
`- ${fileCount} file${fileCount !== 1 ? "s" : ""} changed`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const changes: string[] = [];
|
|
279
|
+
if (insertions > 0) changes.push(`+${insertions}`);
|
|
280
|
+
if (deletions > 0) changes.push(`-${deletions}`);
|
|
281
|
+
if (changes.length > 0) {
|
|
282
|
+
parts.push(`- ${changes.join(", ")} lines`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const description = parts.join("\n") || "No description available.";
|
|
289
|
+
return { title, description };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Fallback content when no context available
|
|
294
|
+
*/
|
|
295
|
+
function generateFallbackContent(changedFiles: string[]): GeneratedPRContent {
|
|
296
|
+
const title =
|
|
297
|
+
changedFiles.length > 0
|
|
298
|
+
? `chore: update ${changedFiles[0].split("/").pop() || "files"}`
|
|
299
|
+
: "chore: update code";
|
|
300
|
+
|
|
301
|
+
const description =
|
|
302
|
+
changedFiles.length > 0
|
|
303
|
+
? `Updated ${changedFiles.length} file${changedFiles.length !== 1 ? "s" : ""}.`
|
|
304
|
+
: "No changes detected.";
|
|
305
|
+
|
|
306
|
+
return { title, description };
|
|
307
|
+
}
|