@atercates/claude-deck 0.2.4 → 0.2.6
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/app/api/sessions/[id]/fork/route.ts +0 -1
- package/app/api/sessions/[id]/route.ts +0 -5
- package/app/api/sessions/[id]/summarize/route.ts +2 -3
- package/app/api/sessions/route.ts +2 -11
- package/app/api/system/route.ts +2 -0
- package/app/page.tsx +4 -45
- package/components/ClaudeProjects/ClaudeProjectCard.tsx +19 -31
- package/components/ClaudeProjects/ClaudeSessionCard.tsx +20 -31
- package/components/NewSessionDialog/AdvancedSettings.tsx +3 -12
- package/components/NewSessionDialog/NewSessionDialog.types.ts +0 -10
- package/components/NewSessionDialog/ProjectSelector.tsx +2 -7
- package/components/NewSessionDialog/hooks/useNewSessionForm.ts +3 -36
- package/components/NewSessionDialog/index.tsx +0 -7
- package/components/Pane/OpenInVSCode.tsx +6 -1
- package/components/Projects/index.ts +0 -1
- package/components/SessionList/hooks/useSessionListMutations.ts +0 -35
- package/components/views/DesktopView.tsx +1 -6
- package/components/views/MobileView.tsx +6 -1
- package/components/views/types.ts +1 -1
- package/data/sessions/index.ts +0 -1
- package/data/sessions/queries.ts +1 -27
- package/hooks/useSessions.ts +0 -12
- package/lib/db/queries.ts +4 -64
- package/lib/db/types.ts +0 -8
- package/lib/orchestration.ts +10 -15
- package/lib/providers/registry.ts +2 -56
- package/lib/providers.ts +19 -100
- package/lib/status-monitor.ts +40 -15
- package/package.json +1 -1
- package/server.ts +1 -1
- package/app/api/groups/[...path]/route.ts +0 -136
- package/app/api/groups/route.ts +0 -93
- package/components/NewSessionDialog/AgentSelector.tsx +0 -37
- package/components/Projects/ProjectCard.tsx +0 -276
- package/components/TmuxSessions.tsx +0 -132
- package/data/groups/index.ts +0 -1
- package/data/groups/mutations.ts +0 -95
- package/hooks/useGroups.ts +0 -37
- package/hooks/useKeybarVisibility.ts +0 -42
- package/lib/claude/process-manager.ts +0 -278
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useCallback } from "react";
|
|
4
|
-
import { Button } from "./ui/button";
|
|
5
|
-
import { Badge } from "./ui/badge";
|
|
6
|
-
import { RefreshCw, Terminal, MonitorUp } from "lucide-react";
|
|
7
|
-
import { cn } from "@/lib/utils";
|
|
8
|
-
|
|
9
|
-
interface TmuxSession {
|
|
10
|
-
name: string;
|
|
11
|
-
windows: number;
|
|
12
|
-
created: string;
|
|
13
|
-
attached: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface TmuxSessionsProps {
|
|
17
|
-
onAttach: (sessionName: string) => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function TmuxSessions({ onAttach }: TmuxSessionsProps) {
|
|
21
|
-
const [sessions, setSessions] = useState<TmuxSession[]>([]);
|
|
22
|
-
const [loading, setLoading] = useState(false);
|
|
23
|
-
const [error, setError] = useState<string | null>(null);
|
|
24
|
-
|
|
25
|
-
const fetchSessions = useCallback(async () => {
|
|
26
|
-
setLoading(true);
|
|
27
|
-
setError(null);
|
|
28
|
-
try {
|
|
29
|
-
const res = await fetch("/api/exec", {
|
|
30
|
-
method: "POST",
|
|
31
|
-
headers: { "Content-Type": "application/json" },
|
|
32
|
-
body: JSON.stringify({
|
|
33
|
-
command:
|
|
34
|
-
"tmux list-sessions -F '#{session_name}|#{session_windows}|#{session_created}|#{session_attached}' 2>/dev/null || echo ''",
|
|
35
|
-
}),
|
|
36
|
-
});
|
|
37
|
-
const data = await res.json();
|
|
38
|
-
|
|
39
|
-
if (data.success && data.output.trim()) {
|
|
40
|
-
const parsed = data.output
|
|
41
|
-
.trim()
|
|
42
|
-
.split("\n")
|
|
43
|
-
.filter((line: string) => line.includes("|"))
|
|
44
|
-
.map((line: string) => {
|
|
45
|
-
const [name, windows, created, attached] = line.split("|");
|
|
46
|
-
return {
|
|
47
|
-
name,
|
|
48
|
-
windows: parseInt(windows),
|
|
49
|
-
created: new Date(parseInt(created) * 1000).toLocaleString(),
|
|
50
|
-
attached: attached === "1",
|
|
51
|
-
};
|
|
52
|
-
});
|
|
53
|
-
setSessions(parsed);
|
|
54
|
-
} else {
|
|
55
|
-
setSessions([]);
|
|
56
|
-
}
|
|
57
|
-
} catch (err) {
|
|
58
|
-
console.error("Failed to fetch tmux sessions:", err);
|
|
59
|
-
setError("Failed to load");
|
|
60
|
-
setSessions([]);
|
|
61
|
-
} finally {
|
|
62
|
-
setLoading(false);
|
|
63
|
-
}
|
|
64
|
-
}, []);
|
|
65
|
-
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
fetchSessions();
|
|
68
|
-
// Refresh every 30 seconds
|
|
69
|
-
const interval = setInterval(fetchSessions, 30000);
|
|
70
|
-
return () => clearInterval(interval);
|
|
71
|
-
}, [fetchSessions]);
|
|
72
|
-
|
|
73
|
-
if (sessions.length === 0 && !loading && !error) {
|
|
74
|
-
return null; // Don't show section if no tmux sessions
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<div className="border-border border-b">
|
|
79
|
-
<div className="flex items-center justify-between px-4 py-2">
|
|
80
|
-
<div className="flex items-center gap-2">
|
|
81
|
-
<Terminal className="text-muted-foreground h-4 w-4" />
|
|
82
|
-
<span className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
|
|
83
|
-
Tmux Sessions
|
|
84
|
-
</span>
|
|
85
|
-
</div>
|
|
86
|
-
<Button
|
|
87
|
-
variant="ghost"
|
|
88
|
-
size="icon-sm"
|
|
89
|
-
onClick={fetchSessions}
|
|
90
|
-
disabled={loading}
|
|
91
|
-
className="h-6 w-6"
|
|
92
|
-
>
|
|
93
|
-
<RefreshCw className={cn("h-3 w-3", loading && "animate-spin")} />
|
|
94
|
-
</Button>
|
|
95
|
-
</div>
|
|
96
|
-
|
|
97
|
-
<div className="space-y-1 px-4 pb-3">
|
|
98
|
-
{error && <p className="text-destructive text-xs">{error}</p>}
|
|
99
|
-
{sessions.map((session) => (
|
|
100
|
-
<button
|
|
101
|
-
key={session.name}
|
|
102
|
-
onClick={() => onAttach(session.name)}
|
|
103
|
-
className={cn(
|
|
104
|
-
"flex w-full items-center justify-between rounded-md p-2 text-left transition-colors",
|
|
105
|
-
"hover:bg-primary/10 border",
|
|
106
|
-
session.attached
|
|
107
|
-
? "border-primary/50 bg-primary/5"
|
|
108
|
-
: "border-transparent"
|
|
109
|
-
)}
|
|
110
|
-
>
|
|
111
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
112
|
-
<MonitorUp className="text-primary h-4 w-4 flex-shrink-0" />
|
|
113
|
-
<span className="truncate text-sm font-medium">
|
|
114
|
-
{session.name}
|
|
115
|
-
</span>
|
|
116
|
-
</div>
|
|
117
|
-
<div className="flex flex-shrink-0 items-center gap-2">
|
|
118
|
-
<span className="text-muted-foreground text-xs">
|
|
119
|
-
{session.windows}w
|
|
120
|
-
</span>
|
|
121
|
-
{session.attached && (
|
|
122
|
-
<Badge variant="success" className="px-1 py-0 text-[10px]">
|
|
123
|
-
attached
|
|
124
|
-
</Badge>
|
|
125
|
-
)}
|
|
126
|
-
</div>
|
|
127
|
-
</button>
|
|
128
|
-
))}
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
);
|
|
132
|
-
}
|
package/data/groups/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { useToggleGroup, useCreateGroup, useDeleteGroup } from "./mutations";
|
package/data/groups/mutations.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
-
import { sessionKeys } from "../sessions/keys";
|
|
3
|
-
|
|
4
|
-
export function useToggleGroup() {
|
|
5
|
-
const queryClient = useQueryClient();
|
|
6
|
-
|
|
7
|
-
return useMutation({
|
|
8
|
-
mutationFn: async ({
|
|
9
|
-
path,
|
|
10
|
-
expanded,
|
|
11
|
-
}: {
|
|
12
|
-
path: string;
|
|
13
|
-
expanded: boolean;
|
|
14
|
-
}) => {
|
|
15
|
-
const res = await fetch(`/api/groups/${encodeURIComponent(path)}`, {
|
|
16
|
-
method: "PATCH",
|
|
17
|
-
headers: { "Content-Type": "application/json" },
|
|
18
|
-
body: JSON.stringify({ expanded }),
|
|
19
|
-
});
|
|
20
|
-
if (!res.ok) throw new Error("Failed to toggle group");
|
|
21
|
-
return res.json();
|
|
22
|
-
},
|
|
23
|
-
onMutate: async ({ path, expanded }) => {
|
|
24
|
-
await queryClient.cancelQueries({ queryKey: sessionKeys.list() });
|
|
25
|
-
const previous = queryClient.getQueryData(sessionKeys.list());
|
|
26
|
-
queryClient.setQueryData(
|
|
27
|
-
sessionKeys.list(),
|
|
28
|
-
(
|
|
29
|
-
old:
|
|
30
|
-
| {
|
|
31
|
-
sessions: unknown[];
|
|
32
|
-
groups: Array<{ path: string; expanded: boolean }>;
|
|
33
|
-
}
|
|
34
|
-
| undefined
|
|
35
|
-
) =>
|
|
36
|
-
old
|
|
37
|
-
? {
|
|
38
|
-
...old,
|
|
39
|
-
groups: old.groups.map((g) =>
|
|
40
|
-
g.path === path ? { ...g, expanded } : g
|
|
41
|
-
),
|
|
42
|
-
}
|
|
43
|
-
: old
|
|
44
|
-
);
|
|
45
|
-
return { previous };
|
|
46
|
-
},
|
|
47
|
-
onError: (_, __, context) => {
|
|
48
|
-
if (context?.previous) {
|
|
49
|
-
queryClient.setQueryData(sessionKeys.list(), context.previous);
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function useCreateGroup() {
|
|
56
|
-
const queryClient = useQueryClient();
|
|
57
|
-
|
|
58
|
-
return useMutation({
|
|
59
|
-
mutationFn: async ({
|
|
60
|
-
name,
|
|
61
|
-
parentPath,
|
|
62
|
-
}: {
|
|
63
|
-
name: string;
|
|
64
|
-
parentPath?: string;
|
|
65
|
-
}) => {
|
|
66
|
-
const res = await fetch("/api/groups", {
|
|
67
|
-
method: "POST",
|
|
68
|
-
headers: { "Content-Type": "application/json" },
|
|
69
|
-
body: JSON.stringify({ name, parentPath }),
|
|
70
|
-
});
|
|
71
|
-
if (!res.ok) throw new Error("Failed to create group");
|
|
72
|
-
return res.json();
|
|
73
|
-
},
|
|
74
|
-
onSuccess: () => {
|
|
75
|
-
queryClient.invalidateQueries({ queryKey: sessionKeys.list() });
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function useDeleteGroup() {
|
|
81
|
-
const queryClient = useQueryClient();
|
|
82
|
-
|
|
83
|
-
return useMutation({
|
|
84
|
-
mutationFn: async (path: string) => {
|
|
85
|
-
const res = await fetch(`/api/groups/${encodeURIComponent(path)}`, {
|
|
86
|
-
method: "DELETE",
|
|
87
|
-
});
|
|
88
|
-
if (!res.ok) throw new Error("Failed to delete group");
|
|
89
|
-
return res.json();
|
|
90
|
-
},
|
|
91
|
-
onSuccess: () => {
|
|
92
|
-
queryClient.invalidateQueries({ queryKey: sessionKeys.list() });
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
}
|
package/hooks/useGroups.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
2
|
-
import { useToggleGroup, useCreateGroup, useDeleteGroup } from "@/data/groups";
|
|
3
|
-
|
|
4
|
-
export function useGroups() {
|
|
5
|
-
const toggleMutation = useToggleGroup();
|
|
6
|
-
const createMutation = useCreateGroup();
|
|
7
|
-
const deleteMutation = useDeleteGroup();
|
|
8
|
-
|
|
9
|
-
const toggleGroup = useCallback(
|
|
10
|
-
async (path: string, expanded: boolean) => {
|
|
11
|
-
await toggleMutation.mutateAsync({ path, expanded });
|
|
12
|
-
},
|
|
13
|
-
[toggleMutation]
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
const createGroup = useCallback(
|
|
17
|
-
async (name: string, parentPath?: string) => {
|
|
18
|
-
await createMutation.mutateAsync({ name, parentPath });
|
|
19
|
-
},
|
|
20
|
-
[createMutation]
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
const deleteGroup = useCallback(
|
|
24
|
-
async (path: string) => {
|
|
25
|
-
if (!confirm("Delete this group? Sessions will be moved to parent."))
|
|
26
|
-
return;
|
|
27
|
-
await deleteMutation.mutateAsync(path);
|
|
28
|
-
},
|
|
29
|
-
[deleteMutation]
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
toggleGroup,
|
|
34
|
-
createGroup,
|
|
35
|
-
deleteGroup,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useCallback, useEffect } from "react";
|
|
4
|
-
|
|
5
|
-
const STORAGE_KEY = "agentOS-keybar-visible";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Hook to manage mobile keybar visibility with localStorage persistence.
|
|
9
|
-
* Default: hidden on mobile to maximize terminal space.
|
|
10
|
-
*/
|
|
11
|
-
export function useKeybarVisibility() {
|
|
12
|
-
const [isVisible, setIsVisible] = useState(false);
|
|
13
|
-
|
|
14
|
-
// Load persisted state on mount
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
if (typeof window === "undefined") return;
|
|
17
|
-
const stored = localStorage.getItem(STORAGE_KEY);
|
|
18
|
-
if (stored === "true") {
|
|
19
|
-
setIsVisible(true);
|
|
20
|
-
}
|
|
21
|
-
}, []);
|
|
22
|
-
|
|
23
|
-
const toggle = useCallback(() => {
|
|
24
|
-
setIsVisible((prev) => {
|
|
25
|
-
const next = !prev;
|
|
26
|
-
localStorage.setItem(STORAGE_KEY, String(next));
|
|
27
|
-
return next;
|
|
28
|
-
});
|
|
29
|
-
}, []);
|
|
30
|
-
|
|
31
|
-
const show = useCallback(() => {
|
|
32
|
-
setIsVisible(true);
|
|
33
|
-
localStorage.setItem(STORAGE_KEY, "true");
|
|
34
|
-
}, []);
|
|
35
|
-
|
|
36
|
-
const hide = useCallback(() => {
|
|
37
|
-
setIsVisible(false);
|
|
38
|
-
localStorage.setItem(STORAGE_KEY, "false");
|
|
39
|
-
}, []);
|
|
40
|
-
|
|
41
|
-
return { isVisible, toggle, show, hide };
|
|
42
|
-
}
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import { spawn, ChildProcess } from "child_process";
|
|
2
|
-
import { WebSocket } from "ws";
|
|
3
|
-
import { StreamParser } from "./stream-parser";
|
|
4
|
-
import { queries } from "../db";
|
|
5
|
-
import type { ClaudeSessionOptions, ClientEvent } from "./types";
|
|
6
|
-
|
|
7
|
-
interface ManagedSession {
|
|
8
|
-
process: ChildProcess | null;
|
|
9
|
-
parser: StreamParser;
|
|
10
|
-
clients: Set<WebSocket>;
|
|
11
|
-
status: "idle" | "running" | "waiting" | "error";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class ClaudeProcessManager {
|
|
15
|
-
private sessions: Map<string, ManagedSession> = new Map();
|
|
16
|
-
|
|
17
|
-
// Register a WebSocket client for a session
|
|
18
|
-
registerClient(sessionId: string, ws: WebSocket): void {
|
|
19
|
-
let session = this.sessions.get(sessionId);
|
|
20
|
-
|
|
21
|
-
if (!session) {
|
|
22
|
-
session = {
|
|
23
|
-
process: null,
|
|
24
|
-
parser: new StreamParser(sessionId),
|
|
25
|
-
clients: new Set(),
|
|
26
|
-
status: "idle",
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// Set up parser event handlers
|
|
30
|
-
session.parser.on("event", (event: ClientEvent) => {
|
|
31
|
-
this.broadcastToSession(sessionId, event);
|
|
32
|
-
this.handleEvent(sessionId, event);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
session.parser.on("parse_error", (error) => {
|
|
36
|
-
this.broadcastToSession(sessionId, {
|
|
37
|
-
type: "error",
|
|
38
|
-
sessionId,
|
|
39
|
-
timestamp: new Date().toISOString(),
|
|
40
|
-
data: { error: `Parse error: ${error.error}` },
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
this.sessions.set(sessionId, session);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
session.clients.add(ws);
|
|
48
|
-
|
|
49
|
-
// Send current status
|
|
50
|
-
ws.send(
|
|
51
|
-
JSON.stringify({
|
|
52
|
-
type: "status",
|
|
53
|
-
sessionId,
|
|
54
|
-
timestamp: new Date().toISOString(),
|
|
55
|
-
data: { status: session.status },
|
|
56
|
-
})
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Unregister a WebSocket client
|
|
61
|
-
unregisterClient(sessionId: string, ws: WebSocket): void {
|
|
62
|
-
const session = this.sessions.get(sessionId);
|
|
63
|
-
if (session) {
|
|
64
|
-
session.clients.delete(ws);
|
|
65
|
-
|
|
66
|
-
// Clean up if no clients remain and process not running
|
|
67
|
-
if (session.clients.size === 0 && !session.process) {
|
|
68
|
-
this.sessions.delete(sessionId);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Send a prompt to Claude
|
|
74
|
-
async sendPrompt(
|
|
75
|
-
sessionId: string,
|
|
76
|
-
prompt: string,
|
|
77
|
-
options: ClaudeSessionOptions = {}
|
|
78
|
-
): Promise<void> {
|
|
79
|
-
const session = this.sessions.get(sessionId);
|
|
80
|
-
if (!session) {
|
|
81
|
-
throw new Error(`Session ${sessionId} not found`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (session.process) {
|
|
85
|
-
throw new Error(`Session ${sessionId} already has a running process`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Build Claude CLI command
|
|
89
|
-
const args = ["-p", "--output-format", "stream-json", "--verbose"];
|
|
90
|
-
|
|
91
|
-
// Add model if specified
|
|
92
|
-
if (options.model) {
|
|
93
|
-
args.push("--model", options.model);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Handle session continuity
|
|
97
|
-
const dbSession = await queries.getSession(sessionId);
|
|
98
|
-
|
|
99
|
-
if (dbSession?.claude_session_id) {
|
|
100
|
-
// Resume existing Claude session
|
|
101
|
-
args.push("--resume", dbSession.claude_session_id);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Add system prompt if specified
|
|
105
|
-
if (options.systemPrompt) {
|
|
106
|
-
args.push("--system-prompt", options.systemPrompt);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Add the prompt
|
|
110
|
-
args.push(prompt);
|
|
111
|
-
|
|
112
|
-
// Spawn Claude process
|
|
113
|
-
const cwd =
|
|
114
|
-
options.workingDirectory ||
|
|
115
|
-
dbSession?.working_directory?.replace("~", process.env.HOME || "") ||
|
|
116
|
-
process.env.HOME ||
|
|
117
|
-
"/";
|
|
118
|
-
|
|
119
|
-
console.log(`Spawning Claude for session ${sessionId}:`, args.join(" "));
|
|
120
|
-
console.log(`CWD: ${cwd}`);
|
|
121
|
-
|
|
122
|
-
// Reset parser for new conversation turn
|
|
123
|
-
session.parser = new StreamParser(sessionId);
|
|
124
|
-
session.parser.on("event", (event: ClientEvent) => {
|
|
125
|
-
console.log(
|
|
126
|
-
`Parser event [${sessionId}]:`,
|
|
127
|
-
event.type,
|
|
128
|
-
JSON.stringify(event.data).substring(0, 100)
|
|
129
|
-
);
|
|
130
|
-
this.broadcastToSession(sessionId, event);
|
|
131
|
-
this.handleEvent(sessionId, event);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// Find claude binary path
|
|
135
|
-
const claudePath =
|
|
136
|
-
process.env.HOME + "/.nvm/versions/node/v20.19.0/bin/claude";
|
|
137
|
-
|
|
138
|
-
const claudeProcess = spawn(claudePath, args, {
|
|
139
|
-
cwd,
|
|
140
|
-
env: {
|
|
141
|
-
...process.env,
|
|
142
|
-
PATH: `/usr/local/bin:/opt/homebrew/bin:${process.env.PATH}`,
|
|
143
|
-
},
|
|
144
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
session.process = claudeProcess;
|
|
148
|
-
session.status = "running";
|
|
149
|
-
this.updateDbStatus(sessionId, "running");
|
|
150
|
-
|
|
151
|
-
this.broadcastToSession(sessionId, {
|
|
152
|
-
type: "status",
|
|
153
|
-
sessionId,
|
|
154
|
-
timestamp: new Date().toISOString(),
|
|
155
|
-
data: { status: "running" },
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Handle stdout (stream-json output)
|
|
159
|
-
claudeProcess.stdout?.on("data", (data: Buffer) => {
|
|
160
|
-
const text = data.toString();
|
|
161
|
-
console.log(`Claude stdout [${sessionId}]:`, text.substring(0, 200));
|
|
162
|
-
session.parser.write(text);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Handle stderr (errors and other output)
|
|
166
|
-
claudeProcess.stderr?.on("data", (data: Buffer) => {
|
|
167
|
-
const text = data.toString();
|
|
168
|
-
console.error(`Claude stderr [${sessionId}]:`, text);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
claudeProcess.on("error", (err) => {
|
|
172
|
-
console.error(`Claude spawn error [${sessionId}]:`, err);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Handle process exit
|
|
176
|
-
claudeProcess.on("close", (code) => {
|
|
177
|
-
console.log(
|
|
178
|
-
`Claude process exited for session ${sessionId} with code ${code}`
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
session.parser.end();
|
|
182
|
-
session.process = null;
|
|
183
|
-
session.status = code === 0 ? "idle" : "error";
|
|
184
|
-
|
|
185
|
-
this.updateDbStatus(sessionId, session.status);
|
|
186
|
-
|
|
187
|
-
this.broadcastToSession(sessionId, {
|
|
188
|
-
type: "status",
|
|
189
|
-
sessionId,
|
|
190
|
-
timestamp: new Date().toISOString(),
|
|
191
|
-
data: { status: session.status, exitCode: code || 0 },
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
claudeProcess.on("error", (err) => {
|
|
196
|
-
console.error(`Claude process error for session ${sessionId}:`, err);
|
|
197
|
-
|
|
198
|
-
session.process = null;
|
|
199
|
-
session.status = "error";
|
|
200
|
-
|
|
201
|
-
this.updateDbStatus(sessionId, "error");
|
|
202
|
-
|
|
203
|
-
this.broadcastToSession(sessionId, {
|
|
204
|
-
type: "error",
|
|
205
|
-
sessionId,
|
|
206
|
-
timestamp: new Date().toISOString(),
|
|
207
|
-
data: { error: err.message },
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Cancel a running Claude process
|
|
213
|
-
cancelSession(sessionId: string): void {
|
|
214
|
-
const session = this.sessions.get(sessionId);
|
|
215
|
-
if (session?.process) {
|
|
216
|
-
session.process.kill("SIGTERM");
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Get session status
|
|
221
|
-
getSessionStatus(
|
|
222
|
-
sessionId: string
|
|
223
|
-
): "idle" | "running" | "waiting" | "error" | null {
|
|
224
|
-
return this.sessions.get(sessionId)?.status ?? null;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Broadcast event to all clients of a session
|
|
228
|
-
private broadcastToSession(sessionId: string, event: ClientEvent): void {
|
|
229
|
-
const session = this.sessions.get(sessionId);
|
|
230
|
-
if (!session) {
|
|
231
|
-
console.log(`No session found for broadcast: ${sessionId}`);
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
console.log(
|
|
236
|
-
`Broadcasting to ${session.clients.size} clients for session ${sessionId}`
|
|
237
|
-
);
|
|
238
|
-
const message = JSON.stringify(event);
|
|
239
|
-
for (const client of session.clients) {
|
|
240
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
241
|
-
client.send(message);
|
|
242
|
-
console.log(`Sent message to client`);
|
|
243
|
-
} else {
|
|
244
|
-
console.log(`Client not open, state: ${client.readyState}`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Handle events for persistence
|
|
250
|
-
private handleEvent(sessionId: string, event: ClientEvent): void {
|
|
251
|
-
switch (event.type) {
|
|
252
|
-
case "init": {
|
|
253
|
-
// Store Claude's session ID for future --resume
|
|
254
|
-
const claudeSessionId = event.data.claudeSessionId;
|
|
255
|
-
if (claudeSessionId) {
|
|
256
|
-
queries.updateSessionClaudeId(claudeSessionId, sessionId);
|
|
257
|
-
}
|
|
258
|
-
break;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
case "complete": {
|
|
262
|
-
// Update session timestamp
|
|
263
|
-
queries.updateSessionStatus("idle", sessionId);
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
case "error": {
|
|
268
|
-
queries.updateSessionStatus("error", sessionId);
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Update session status in database
|
|
275
|
-
private updateDbStatus(sessionId: string, status: string): void {
|
|
276
|
-
queries.updateSessionStatus(status, sessionId);
|
|
277
|
-
}
|
|
278
|
-
}
|