@atercates/claude-deck 0.2.4 → 0.2.5

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.
Files changed (38) hide show
  1. package/app/api/sessions/[id]/fork/route.ts +0 -1
  2. package/app/api/sessions/[id]/route.ts +0 -5
  3. package/app/api/sessions/[id]/summarize/route.ts +2 -3
  4. package/app/api/sessions/route.ts +2 -11
  5. package/app/page.tsx +4 -45
  6. package/components/ClaudeProjects/ClaudeProjectCard.tsx +19 -31
  7. package/components/ClaudeProjects/ClaudeSessionCard.tsx +20 -31
  8. package/components/NewSessionDialog/AdvancedSettings.tsx +3 -12
  9. package/components/NewSessionDialog/NewSessionDialog.types.ts +0 -10
  10. package/components/NewSessionDialog/ProjectSelector.tsx +2 -7
  11. package/components/NewSessionDialog/hooks/useNewSessionForm.ts +3 -36
  12. package/components/NewSessionDialog/index.tsx +0 -7
  13. package/components/Projects/index.ts +0 -1
  14. package/components/SessionList/hooks/useSessionListMutations.ts +0 -35
  15. package/components/views/DesktopView.tsx +1 -6
  16. package/components/views/MobileView.tsx +6 -1
  17. package/components/views/types.ts +1 -1
  18. package/data/sessions/index.ts +0 -1
  19. package/data/sessions/queries.ts +1 -27
  20. package/hooks/useSessions.ts +0 -12
  21. package/lib/db/queries.ts +4 -64
  22. package/lib/db/types.ts +0 -8
  23. package/lib/orchestration.ts +10 -15
  24. package/lib/providers/registry.ts +2 -56
  25. package/lib/providers.ts +19 -100
  26. package/lib/status-monitor.ts +40 -15
  27. package/package.json +1 -1
  28. package/server.ts +1 -1
  29. package/app/api/groups/[...path]/route.ts +0 -136
  30. package/app/api/groups/route.ts +0 -93
  31. package/components/NewSessionDialog/AgentSelector.tsx +0 -37
  32. package/components/Projects/ProjectCard.tsx +0 -276
  33. package/components/TmuxSessions.tsx +0 -132
  34. package/data/groups/index.ts +0 -1
  35. package/data/groups/mutations.ts +0 -95
  36. package/hooks/useGroups.ts +0 -37
  37. package/hooks/useKeybarVisibility.ts +0 -42
  38. 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
- }
@@ -1 +0,0 @@
1
- export { useToggleGroup, useCreateGroup, useDeleteGroup } from "./mutations";
@@ -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
- }
@@ -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
- }