@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,209 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { queries, type Session, type Group } from "@/lib/db";
|
|
4
|
+
import { isValidAgentType, type AgentType } from "@/lib/providers";
|
|
5
|
+
import { createWorktree } from "@/lib/worktrees";
|
|
6
|
+
import { setupWorktree, type SetupResult } from "@/lib/env-setup";
|
|
7
|
+
import { findAvailablePort } from "@/lib/ports";
|
|
8
|
+
import { runInBackground } from "@/lib/async-operations";
|
|
9
|
+
import { getProject } from "@/lib/projects";
|
|
10
|
+
|
|
11
|
+
// GET /api/sessions - List all sessions and groups
|
|
12
|
+
export async function GET() {
|
|
13
|
+
try {
|
|
14
|
+
const sessions = await queries.getAllSessions();
|
|
15
|
+
const groups = await queries.getAllGroups();
|
|
16
|
+
|
|
17
|
+
// Convert expanded from 0/1 to boolean
|
|
18
|
+
const formattedGroups = groups.map((g) => ({
|
|
19
|
+
...g,
|
|
20
|
+
expanded: Boolean(g.expanded),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
return NextResponse.json({ sessions, groups: formattedGroups });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error("Error fetching sessions:", error);
|
|
26
|
+
return NextResponse.json(
|
|
27
|
+
{ error: "Failed to fetch sessions" },
|
|
28
|
+
{ status: 500 }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Generate a unique session name
|
|
34
|
+
async function generateSessionName(): Promise<string> {
|
|
35
|
+
const sessions = await queries.getAllSessions();
|
|
36
|
+
const existingNumbers = sessions
|
|
37
|
+
.map((s) => {
|
|
38
|
+
const match = s.name.match(/^Session (\d+)$/);
|
|
39
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
40
|
+
})
|
|
41
|
+
.filter((n) => n > 0);
|
|
42
|
+
|
|
43
|
+
const nextNumber =
|
|
44
|
+
existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1;
|
|
45
|
+
return `Session ${nextNumber}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// POST /api/sessions - Create new session
|
|
49
|
+
export async function POST(request: NextRequest) {
|
|
50
|
+
try {
|
|
51
|
+
const body = await request.json();
|
|
52
|
+
|
|
53
|
+
const {
|
|
54
|
+
name: providedName,
|
|
55
|
+
workingDirectory = "~",
|
|
56
|
+
parentSessionId = null,
|
|
57
|
+
model = "sonnet",
|
|
58
|
+
systemPrompt = null,
|
|
59
|
+
groupPath = "sessions",
|
|
60
|
+
claudeSessionId = null,
|
|
61
|
+
agentType: rawAgentType = "claude",
|
|
62
|
+
autoApprove = false,
|
|
63
|
+
projectId = "uncategorized",
|
|
64
|
+
// Worktree options
|
|
65
|
+
useWorktree = false,
|
|
66
|
+
featureName = null,
|
|
67
|
+
baseBranch = "main",
|
|
68
|
+
// Tmux option
|
|
69
|
+
useTmux = true,
|
|
70
|
+
// Initial prompt to send when session starts
|
|
71
|
+
initialPrompt = null,
|
|
72
|
+
} = body;
|
|
73
|
+
|
|
74
|
+
// Validate agent type
|
|
75
|
+
const agentType: AgentType = isValidAgentType(rawAgentType)
|
|
76
|
+
? rawAgentType
|
|
77
|
+
: "claude";
|
|
78
|
+
|
|
79
|
+
// Auto-generate name if not provided
|
|
80
|
+
const name =
|
|
81
|
+
providedName?.trim() ||
|
|
82
|
+
(featureName ? featureName : await generateSessionName());
|
|
83
|
+
|
|
84
|
+
const id = randomUUID();
|
|
85
|
+
|
|
86
|
+
// Handle worktree creation if requested
|
|
87
|
+
let worktreePath: string | null = null;
|
|
88
|
+
let branchName: string | null = null;
|
|
89
|
+
let actualWorkingDirectory = workingDirectory;
|
|
90
|
+
let port: number | null = null;
|
|
91
|
+
let setupResult: SetupResult | null = null;
|
|
92
|
+
|
|
93
|
+
if (useWorktree && featureName) {
|
|
94
|
+
try {
|
|
95
|
+
const worktreeInfo = await createWorktree({
|
|
96
|
+
projectPath: workingDirectory,
|
|
97
|
+
featureName,
|
|
98
|
+
baseBranch,
|
|
99
|
+
});
|
|
100
|
+
worktreePath = worktreeInfo.worktreePath;
|
|
101
|
+
branchName = worktreeInfo.branchName;
|
|
102
|
+
actualWorkingDirectory = worktreeInfo.worktreePath;
|
|
103
|
+
|
|
104
|
+
// Find an available port for the dev server
|
|
105
|
+
port = await findAvailablePort();
|
|
106
|
+
|
|
107
|
+
// Run environment setup in background (non-blocking)
|
|
108
|
+
// This allows instant UI feedback while npm install runs async
|
|
109
|
+
const capturedWorktreePath = worktreeInfo.worktreePath;
|
|
110
|
+
const capturedSourcePath = workingDirectory;
|
|
111
|
+
const capturedPort = port;
|
|
112
|
+
runInBackground(async () => {
|
|
113
|
+
const result = await setupWorktree({
|
|
114
|
+
worktreePath: capturedWorktreePath,
|
|
115
|
+
sourcePath: capturedSourcePath,
|
|
116
|
+
port: capturedPort,
|
|
117
|
+
});
|
|
118
|
+
console.log("Worktree setup completed:", {
|
|
119
|
+
port: capturedPort,
|
|
120
|
+
envFilesCopied: result.envFilesCopied,
|
|
121
|
+
stepsRun: result.steps.length,
|
|
122
|
+
success: result.success,
|
|
123
|
+
});
|
|
124
|
+
}, `setup-worktree-${id}`);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const message =
|
|
127
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
128
|
+
return NextResponse.json(
|
|
129
|
+
{ error: `Failed to create worktree: ${message}` },
|
|
130
|
+
{ status: 400 }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const tmuxName = useTmux ? `${agentType}-${id}` : null;
|
|
136
|
+
await queries.createSession(
|
|
137
|
+
id,
|
|
138
|
+
name,
|
|
139
|
+
tmuxName,
|
|
140
|
+
actualWorkingDirectory,
|
|
141
|
+
parentSessionId,
|
|
142
|
+
model,
|
|
143
|
+
systemPrompt,
|
|
144
|
+
groupPath,
|
|
145
|
+
agentType,
|
|
146
|
+
autoApprove,
|
|
147
|
+
projectId
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Set worktree info if created
|
|
151
|
+
if (worktreePath) {
|
|
152
|
+
await queries.updateSessionWorktree(
|
|
153
|
+
worktreePath,
|
|
154
|
+
branchName,
|
|
155
|
+
baseBranch,
|
|
156
|
+
port,
|
|
157
|
+
id
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Set claude_session_id if provided (for importing external sessions)
|
|
162
|
+
if (claudeSessionId) {
|
|
163
|
+
const { getDb } = await import("@/lib/db");
|
|
164
|
+
getDb()
|
|
165
|
+
.prepare("UPDATE sessions SET claude_session_id = ? WHERE id = ?")
|
|
166
|
+
.run(claudeSessionId, id);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Messages are no longer stored in our DB - skipping message copy for forked sessions
|
|
170
|
+
|
|
171
|
+
const session = await queries.getSession(id);
|
|
172
|
+
|
|
173
|
+
// Get project's initial prompt if available
|
|
174
|
+
const project = projectId ? await getProject(projectId) : null;
|
|
175
|
+
const projectInitialPrompt = project?.initial_prompt?.trim();
|
|
176
|
+
const sessionInitialPrompt = initialPrompt?.trim();
|
|
177
|
+
|
|
178
|
+
// Combine prompts: project prompt first, then session prompt
|
|
179
|
+
let combinedPrompt: string | undefined;
|
|
180
|
+
if (projectInitialPrompt && sessionInitialPrompt) {
|
|
181
|
+
combinedPrompt = `${projectInitialPrompt}\n\n${sessionInitialPrompt}`;
|
|
182
|
+
} else if (projectInitialPrompt) {
|
|
183
|
+
combinedPrompt = projectInitialPrompt;
|
|
184
|
+
} else if (sessionInitialPrompt) {
|
|
185
|
+
combinedPrompt = sessionInitialPrompt;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Include setup result and initial prompt in response
|
|
189
|
+
const response: {
|
|
190
|
+
session: Session | null;
|
|
191
|
+
setup?: SetupResult;
|
|
192
|
+
initialPrompt?: string;
|
|
193
|
+
} = { session };
|
|
194
|
+
if (setupResult) {
|
|
195
|
+
response.setup = setupResult;
|
|
196
|
+
}
|
|
197
|
+
if (combinedPrompt) {
|
|
198
|
+
response.initialPrompt = combinedPrompt;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return NextResponse.json(response, { status: 201 });
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error("Error creating session:", error);
|
|
204
|
+
return NextResponse.json(
|
|
205
|
+
{ error: "Failed to create session" },
|
|
206
|
+
{ status: 500 }
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import * as os from "os";
|
|
7
|
+
import { statusDetector, type SessionStatus } from "@/lib/status-detector";
|
|
8
|
+
import type { AgentType } from "@/lib/providers";
|
|
9
|
+
import {
|
|
10
|
+
getManagedSessionPattern,
|
|
11
|
+
getProviderIdFromSessionName,
|
|
12
|
+
getSessionIdFromName,
|
|
13
|
+
} from "@/lib/providers/registry";
|
|
14
|
+
import { getDb } from "@/lib/db";
|
|
15
|
+
|
|
16
|
+
const execAsync = promisify(exec);
|
|
17
|
+
|
|
18
|
+
interface SessionStatusResponse {
|
|
19
|
+
sessionName: string;
|
|
20
|
+
status: SessionStatus;
|
|
21
|
+
lastLine?: string;
|
|
22
|
+
claudeSessionId?: string | null;
|
|
23
|
+
agentType?: AgentType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function getTmuxSessions(): Promise<string[]> {
|
|
27
|
+
try {
|
|
28
|
+
const { stdout } = await execAsync(
|
|
29
|
+
"tmux list-sessions -F '#{session_name}' 2>/dev/null || true"
|
|
30
|
+
);
|
|
31
|
+
return stdout.trim().split("\n").filter(Boolean);
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function getTmuxSessionCwd(sessionName: string): Promise<string | null> {
|
|
38
|
+
try {
|
|
39
|
+
const { stdout } = await execAsync(
|
|
40
|
+
`tmux display-message -t "${sessionName}" -p "#{pane_current_path}" 2>/dev/null || echo ""`
|
|
41
|
+
);
|
|
42
|
+
const cwd = stdout.trim();
|
|
43
|
+
return cwd || null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Get Claude session ID from tmux environment variable
|
|
50
|
+
async function getClaudeSessionIdFromEnv(
|
|
51
|
+
sessionName: string
|
|
52
|
+
): Promise<string | null> {
|
|
53
|
+
try {
|
|
54
|
+
const { stdout } = await execAsync(
|
|
55
|
+
`tmux show-environment -t "${sessionName}" CLAUDE_SESSION_ID 2>/dev/null || echo ""`
|
|
56
|
+
);
|
|
57
|
+
const line = stdout.trim();
|
|
58
|
+
if (line.startsWith("CLAUDE_SESSION_ID=")) {
|
|
59
|
+
const sessionId = line.replace("CLAUDE_SESSION_ID=", "");
|
|
60
|
+
if (sessionId && sessionId !== "null") {
|
|
61
|
+
return sessionId;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get Claude session ID by looking at session files on disk
|
|
71
|
+
function getClaudeSessionIdFromFiles(projectPath: string): string | null {
|
|
72
|
+
const home = os.homedir();
|
|
73
|
+
const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(home, ".claude");
|
|
74
|
+
const projectDirName = projectPath.replace(/\//g, "-");
|
|
75
|
+
const projectDir = path.join(claudeDir, "projects", projectDirName);
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(projectDir)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const uuidPattern =
|
|
82
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jsonl$/;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const files = fs.readdirSync(projectDir);
|
|
86
|
+
let mostRecent: string | null = null;
|
|
87
|
+
let mostRecentTime = 0;
|
|
88
|
+
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
if (file.startsWith("agent-")) continue;
|
|
91
|
+
if (!uuidPattern.test(file)) continue;
|
|
92
|
+
|
|
93
|
+
const filePath = path.join(projectDir, file);
|
|
94
|
+
const stat = fs.statSync(filePath);
|
|
95
|
+
|
|
96
|
+
if (stat.mtimeMs > mostRecentTime) {
|
|
97
|
+
mostRecentTime = stat.mtimeMs;
|
|
98
|
+
mostRecent = file.replace(".jsonl", "");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (mostRecent && Date.now() - mostRecentTime < 5 * 60 * 1000) {
|
|
103
|
+
return mostRecent;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const configFile = path.join(claudeDir, ".claude.json");
|
|
107
|
+
if (fs.existsSync(configFile)) {
|
|
108
|
+
try {
|
|
109
|
+
const config = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
110
|
+
if (config.projects?.[projectPath]?.lastSessionId) {
|
|
111
|
+
return config.projects[projectPath].lastSessionId;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// Ignore config parse errors
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function getClaudeSessionId(sessionName: string): Promise<string | null> {
|
|
125
|
+
const envId = await getClaudeSessionIdFromEnv(sessionName);
|
|
126
|
+
if (envId) {
|
|
127
|
+
return envId;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const cwd = await getTmuxSessionCwd(sessionName);
|
|
131
|
+
if (cwd) {
|
|
132
|
+
return getClaudeSessionIdFromFiles(cwd);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function getLastLine(sessionName: string): Promise<string> {
|
|
139
|
+
try {
|
|
140
|
+
const { stdout } = await execAsync(
|
|
141
|
+
`tmux capture-pane -t "${sessionName}" -p -S -5 2>/dev/null || echo ""`
|
|
142
|
+
);
|
|
143
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
144
|
+
return lines.pop() || "";
|
|
145
|
+
} catch {
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// UUID pattern for claude-deck managed sessions (derived from registry)
|
|
151
|
+
const UUID_PATTERN = getManagedSessionPattern();
|
|
152
|
+
|
|
153
|
+
// Track previous statuses to detect changes
|
|
154
|
+
const previousStatuses = new Map<string, SessionStatus>();
|
|
155
|
+
|
|
156
|
+
function getAgentTypeFromSessionName(sessionName: string): AgentType {
|
|
157
|
+
return getProviderIdFromSessionName(sessionName) || "claude";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function GET() {
|
|
161
|
+
try {
|
|
162
|
+
const sessions = await getTmuxSessions();
|
|
163
|
+
|
|
164
|
+
// Get status for claude-deck managed sessions
|
|
165
|
+
const managedSessions = sessions.filter((s) => UUID_PATTERN.test(s));
|
|
166
|
+
|
|
167
|
+
// Use the new status detector
|
|
168
|
+
const statusMap: Record<string, SessionStatusResponse> = {};
|
|
169
|
+
|
|
170
|
+
const db = getDb();
|
|
171
|
+
const sessionsToUpdate: string[] = [];
|
|
172
|
+
|
|
173
|
+
// Process all sessions in parallel for speed
|
|
174
|
+
const sessionPromises = managedSessions.map(async (sessionName) => {
|
|
175
|
+
const [status, claudeSessionId, lastLine] = await Promise.all([
|
|
176
|
+
statusDetector.getStatus(sessionName),
|
|
177
|
+
getClaudeSessionId(sessionName),
|
|
178
|
+
getLastLine(sessionName),
|
|
179
|
+
]);
|
|
180
|
+
const id = getSessionIdFromName(sessionName);
|
|
181
|
+
const agentType = getAgentTypeFromSessionName(sessionName);
|
|
182
|
+
|
|
183
|
+
return { sessionName, id, status, claudeSessionId, lastLine, agentType };
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const results = await Promise.all(sessionPromises);
|
|
187
|
+
|
|
188
|
+
for (const {
|
|
189
|
+
sessionName,
|
|
190
|
+
id,
|
|
191
|
+
status,
|
|
192
|
+
claudeSessionId,
|
|
193
|
+
lastLine,
|
|
194
|
+
agentType,
|
|
195
|
+
} of results) {
|
|
196
|
+
// Track status changes - update DB when session becomes active
|
|
197
|
+
const prevStatus = previousStatuses.get(id);
|
|
198
|
+
if (status === "running" || status === "waiting") {
|
|
199
|
+
if (prevStatus !== status) {
|
|
200
|
+
sessionsToUpdate.push(id);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
previousStatuses.set(id, status);
|
|
204
|
+
|
|
205
|
+
statusMap[id] = {
|
|
206
|
+
sessionName,
|
|
207
|
+
status,
|
|
208
|
+
lastLine,
|
|
209
|
+
claudeSessionId,
|
|
210
|
+
agentType,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Batch update sessions and claude_session_id
|
|
215
|
+
for (const id of sessionsToUpdate) {
|
|
216
|
+
db.prepare(
|
|
217
|
+
"UPDATE sessions SET updated_at = datetime('now') WHERE id = ?"
|
|
218
|
+
).run(id);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const { id, claudeSessionId } of results) {
|
|
222
|
+
if (claudeSessionId) {
|
|
223
|
+
db.prepare(
|
|
224
|
+
"UPDATE sessions SET claude_session_id = ? WHERE id = ? AND (claude_session_id IS NULL OR claude_session_id != ?)"
|
|
225
|
+
).run(claudeSessionId, id, claudeSessionId);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Cleanup old trackers
|
|
230
|
+
statusDetector.cleanup();
|
|
231
|
+
|
|
232
|
+
return NextResponse.json({ statuses: statusMap });
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error("Error getting session statuses:", error);
|
|
235
|
+
return NextResponse.json({ statuses: {} });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
import { queries, type Session } from "@/lib/db";
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
|
|
8
|
+
// POST /api/tmux/kill-all - Kill all ClaudeDeck tmux sessions and remove from database
|
|
9
|
+
export async function POST() {
|
|
10
|
+
try {
|
|
11
|
+
// Get all tmux sessions
|
|
12
|
+
const { stdout } = await execAsync(
|
|
13
|
+
'tmux list-sessions -F "#{session_name}" 2>/dev/null || echo ""',
|
|
14
|
+
{ timeout: 5000 }
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const tmuxSessions = stdout
|
|
18
|
+
.trim()
|
|
19
|
+
.split("\n")
|
|
20
|
+
.filter(
|
|
21
|
+
(s) => s && /^(claude|codex|opencode|gemini|aider|cursor)-/.test(s)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Kill each tmux session
|
|
25
|
+
const killed: string[] = [];
|
|
26
|
+
for (const session of tmuxSessions) {
|
|
27
|
+
try {
|
|
28
|
+
await execAsync(`tmux kill-session -t "${session}"`, { timeout: 5000 });
|
|
29
|
+
killed.push(session);
|
|
30
|
+
} catch {
|
|
31
|
+
// Session might already be dead, continue
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Delete ALL sessions from database
|
|
36
|
+
const dbSessions = await queries.getAllSessions() as Session[];
|
|
37
|
+
for (const session of dbSessions) {
|
|
38
|
+
try {
|
|
39
|
+
await queries.deleteSession(session.id);
|
|
40
|
+
} catch {
|
|
41
|
+
// Continue on error
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return NextResponse.json({
|
|
46
|
+
killed: killed.length,
|
|
47
|
+
sessions: killed,
|
|
48
|
+
deletedFromDb: dbSessions.length,
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error("Error killing tmux sessions:", error);
|
|
52
|
+
return NextResponse.json(
|
|
53
|
+
{ error: "Failed to kill sessions" },
|
|
54
|
+
{ status: 500 }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
|
|
7
|
+
// POST /api/tmux/rename - Rename a tmux session
|
|
8
|
+
export async function POST(request: NextRequest) {
|
|
9
|
+
try {
|
|
10
|
+
const { oldName, newName } = await request.json();
|
|
11
|
+
|
|
12
|
+
if (!oldName || !newName) {
|
|
13
|
+
return NextResponse.json(
|
|
14
|
+
{ error: "oldName and newName are required" },
|
|
15
|
+
{ status: 400 }
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Rename the tmux session
|
|
20
|
+
await execAsync(`tmux rename-session -t "${oldName}" "${newName}"`);
|
|
21
|
+
|
|
22
|
+
return NextResponse.json({ success: true, newName });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error("Error renaming tmux session:", error);
|
|
25
|
+
return NextResponse.json(
|
|
26
|
+
{ error: "Failed to rename tmux session" },
|
|
27
|
+
{ status: 500 }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|