@atercates/claude-deck 0.2.3 → 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 (52) 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/api/sessions/status/acknowledge/route.ts +8 -0
  6. package/app/api/sessions/status/route.ts +2 -233
  7. package/app/page.tsx +6 -13
  8. package/components/ClaudeProjects/ClaudeProjectCard.tsx +19 -31
  9. package/components/ClaudeProjects/ClaudeSessionCard.tsx +20 -31
  10. package/components/NewSessionDialog/AdvancedSettings.tsx +3 -12
  11. package/components/NewSessionDialog/NewSessionDialog.types.ts +0 -10
  12. package/components/NewSessionDialog/ProjectSelector.tsx +2 -7
  13. package/components/NewSessionDialog/hooks/useNewSessionForm.ts +3 -36
  14. package/components/NewSessionDialog/index.tsx +0 -7
  15. package/components/Pane/DesktopTabBar.tsx +62 -28
  16. package/components/Pane/index.tsx +5 -0
  17. package/components/Projects/index.ts +0 -1
  18. package/components/QuickSwitcher.tsx +63 -11
  19. package/components/SessionList/ActiveSessionsSection.tsx +116 -0
  20. package/components/SessionList/hooks/useSessionListMutations.ts +0 -35
  21. package/components/SessionList/index.tsx +9 -1
  22. package/components/SessionStatusBar.tsx +155 -0
  23. package/components/WaitingBanner.tsx +122 -0
  24. package/components/views/DesktopView.tsx +27 -8
  25. package/components/views/MobileView.tsx +6 -1
  26. package/components/views/types.ts +2 -0
  27. package/data/sessions/index.ts +0 -1
  28. package/data/sessions/queries.ts +1 -27
  29. package/data/statuses/queries.ts +68 -34
  30. package/hooks/useSessions.ts +0 -12
  31. package/lib/claude/watcher.ts +28 -5
  32. package/lib/db/queries.ts +4 -64
  33. package/lib/db/types.ts +0 -8
  34. package/lib/hooks/reporter.ts +116 -0
  35. package/lib/hooks/setup.ts +164 -0
  36. package/lib/orchestration.ts +16 -23
  37. package/lib/providers/registry.ts +3 -57
  38. package/lib/providers.ts +19 -100
  39. package/lib/status-monitor.ts +303 -0
  40. package/package.json +1 -1
  41. package/server.ts +5 -1
  42. package/app/api/groups/[...path]/route.ts +0 -136
  43. package/app/api/groups/route.ts +0 -93
  44. package/components/NewSessionDialog/AgentSelector.tsx +0 -37
  45. package/components/Projects/ProjectCard.tsx +0 -276
  46. package/components/TmuxSessions.tsx +0 -132
  47. package/data/groups/index.ts +0 -1
  48. package/data/groups/mutations.ts +0 -95
  49. package/hooks/useGroups.ts +0 -37
  50. package/hooks/useKeybarVisibility.ts +0 -42
  51. package/lib/claude/process-manager.ts +0 -278
  52. package/lib/status-detector.ts +0 -375
@@ -1,37 +0,0 @@
1
- import {
2
- Select,
3
- SelectContent,
4
- SelectItem,
5
- SelectTrigger,
6
- SelectValue,
7
- } from "@/components/ui/select";
8
- import type { AgentType } from "@/lib/providers";
9
- import { AGENT_OPTIONS } from "./NewSessionDialog.types";
10
-
11
- interface AgentSelectorProps {
12
- value: AgentType;
13
- onChange: (value: AgentType) => void;
14
- }
15
-
16
- export function AgentSelector({ value, onChange }: AgentSelectorProps) {
17
- return (
18
- <div className="space-y-2">
19
- <label className="text-sm font-medium">Agent</label>
20
- <Select value={value} onValueChange={(v) => onChange(v as AgentType)}>
21
- <SelectTrigger>
22
- <SelectValue />
23
- </SelectTrigger>
24
- <SelectContent>
25
- {AGENT_OPTIONS.map((option) => (
26
- <SelectItem key={option.value} value={option.value}>
27
- <span className="font-medium">{option.label}</span>
28
- <span className="text-muted-foreground ml-2 text-xs">
29
- {option.description}
30
- </span>
31
- </SelectItem>
32
- ))}
33
- </SelectContent>
34
- </Select>
35
- </div>
36
- );
37
- }
@@ -1,276 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useRef, useEffect } from "react";
4
- import { cn } from "@/lib/utils";
5
- import {
6
- ChevronRight,
7
- ChevronDown,
8
- MoreHorizontal,
9
- Settings,
10
- Plus,
11
- Server,
12
- Trash2,
13
- Pencil,
14
- FolderOpen,
15
- Terminal,
16
- } from "lucide-react";
17
- import { Button } from "@/components/ui/button";
18
- import {
19
- DropdownMenu,
20
- DropdownMenuContent,
21
- DropdownMenuItem,
22
- DropdownMenuSeparator,
23
- DropdownMenuTrigger,
24
- } from "@/components/ui/dropdown-menu";
25
- import {
26
- ContextMenu,
27
- ContextMenuContent,
28
- ContextMenuItem,
29
- ContextMenuSeparator,
30
- ContextMenuTrigger,
31
- } from "@/components/ui/context-menu";
32
- import {
33
- Tooltip,
34
- TooltipContent,
35
- TooltipTrigger,
36
- } from "@/components/ui/tooltip";
37
- import type { Project, DevServer } from "@/lib/db";
38
-
39
- interface ProjectCardProps {
40
- project: Project;
41
- sessionCount: number;
42
- runningDevServers?: DevServer[];
43
- onClick?: () => void;
44
- onToggleExpanded?: (expanded: boolean) => void;
45
- onEdit?: () => void;
46
- onNewSession?: () => void;
47
- onOpenTerminal?: () => void;
48
- onStartDevServer?: () => void;
49
- onOpenInEditor?: () => void;
50
- onDelete?: () => void;
51
- onRename?: (newName: string) => void;
52
- }
53
-
54
- export function ProjectCard({
55
- project,
56
- sessionCount,
57
- runningDevServers = [],
58
- onClick,
59
- onToggleExpanded,
60
- onEdit,
61
- onNewSession,
62
- onOpenTerminal,
63
- onStartDevServer,
64
- onOpenInEditor,
65
- onDelete,
66
- onRename,
67
- }: ProjectCardProps) {
68
- const [isEditing, setIsEditing] = useState(false);
69
- const [editName, setEditName] = useState(project.name);
70
- const inputRef = useRef<HTMLInputElement>(null);
71
- const justStartedEditingRef = useRef(false);
72
-
73
- const hasRunningServers = runningDevServers.length > 0;
74
- // Uncategorized can have New Session, Open Terminal, and Rename, but not Edit/Delete/DevServer
75
- const hasActions = project.is_uncategorized
76
- ? onNewSession || onOpenTerminal || onRename
77
- : onEdit ||
78
- onNewSession ||
79
- onOpenTerminal ||
80
- onStartDevServer ||
81
- onDelete ||
82
- onRename;
83
-
84
- useEffect(() => {
85
- if (isEditing && inputRef.current) {
86
- const input = inputRef.current;
87
- // Mark that we just started editing to ignore immediate blur
88
- justStartedEditingRef.current = true;
89
- // Small timeout to ensure input is fully mounted
90
- setTimeout(() => {
91
- input.focus();
92
- input.select();
93
- // Clear the flag after focus is established
94
- setTimeout(() => {
95
- justStartedEditingRef.current = false;
96
- }, 100);
97
- }, 0);
98
- }
99
- }, [isEditing]);
100
-
101
- const handleRename = () => {
102
- // Ignore blur events that happen immediately after starting to edit
103
- if (justStartedEditingRef.current) return;
104
-
105
- if (editName.trim() && editName !== project.name && onRename) {
106
- onRename(editName.trim());
107
- }
108
- setIsEditing(false);
109
- };
110
-
111
- const handleClick = (_e: React.MouseEvent) => {
112
- if (isEditing) return;
113
- onClick?.();
114
- onToggleExpanded?.(!project.expanded);
115
- };
116
-
117
- const renderMenuItems = (isContextMenu: boolean) => {
118
- const MenuItem = isContextMenu ? ContextMenuItem : DropdownMenuItem;
119
- const MenuSeparator = isContextMenu
120
- ? ContextMenuSeparator
121
- : DropdownMenuSeparator;
122
-
123
- return (
124
- <>
125
- {onNewSession && (
126
- <MenuItem onClick={() => onNewSession()}>
127
- <Plus className="mr-2 h-3 w-3" />
128
- New session
129
- </MenuItem>
130
- )}
131
- {onOpenTerminal && (
132
- <MenuItem onClick={() => onOpenTerminal()}>
133
- <Terminal className="mr-2 h-3 w-3" />
134
- Open terminal
135
- </MenuItem>
136
- )}
137
- {onEdit && (
138
- <MenuItem onClick={() => onEdit()}>
139
- <Settings className="mr-2 h-3 w-3" />
140
- Project settings
141
- </MenuItem>
142
- )}
143
- {onRename && (
144
- <MenuItem onClick={() => setIsEditing(true)}>
145
- <Pencil className="mr-2 h-3 w-3" />
146
- Rename
147
- </MenuItem>
148
- )}
149
- {onOpenInEditor && (
150
- <MenuItem onClick={() => onOpenInEditor()}>
151
- <FolderOpen className="mr-2 h-3 w-3" />
152
- Open in editor
153
- </MenuItem>
154
- )}
155
- {onStartDevServer && (
156
- <>
157
- <MenuSeparator />
158
- <MenuItem onClick={() => onStartDevServer()}>
159
- <Server className="mr-2 h-3 w-3" />
160
- Start dev server
161
- </MenuItem>
162
- </>
163
- )}
164
- {onDelete && (
165
- <>
166
- <MenuSeparator />
167
- <MenuItem
168
- onClick={() => onDelete()}
169
- className="text-red-500 focus:text-red-500"
170
- >
171
- <Trash2 className="mr-2 h-3 w-3" />
172
- Delete project
173
- </MenuItem>
174
- </>
175
- )}
176
- </>
177
- );
178
- };
179
-
180
- const cardContent = (
181
- <div
182
- onClick={handleClick}
183
- className={cn(
184
- "group flex cursor-pointer items-center gap-1 rounded-md px-2 py-1.5",
185
- "min-h-[36px] md:min-h-[28px]",
186
- "hover:bg-accent/50"
187
- )}
188
- >
189
- {/* Expand/collapse toggle */}
190
- <button className="flex-shrink-0 p-0.5">
191
- {project.expanded ? (
192
- <ChevronDown className="text-muted-foreground h-4 w-4" />
193
- ) : (
194
- <ChevronRight className="text-muted-foreground h-4 w-4" />
195
- )}
196
- </button>
197
-
198
- {/* Project name */}
199
- {isEditing ? (
200
- <input
201
- ref={inputRef}
202
- type="text"
203
- value={editName}
204
- onChange={(e) => setEditName(e.target.value)}
205
- onBlur={handleRename}
206
- onKeyDown={(e) => {
207
- if (e.key === "Enter") handleRename();
208
- if (e.key === "Escape") {
209
- setEditName(project.name);
210
- setIsEditing(false);
211
- }
212
- }}
213
- onClick={(e) => e.stopPropagation()}
214
- className="border-primary min-w-0 flex-1 border-b bg-transparent text-sm font-medium outline-none"
215
- />
216
- ) : (
217
- <span className="min-w-0 flex-1 truncate text-sm font-medium">
218
- {project.name}
219
- </span>
220
- )}
221
-
222
- {/* Running servers indicator */}
223
- {hasRunningServers && (
224
- <Tooltip>
225
- <TooltipTrigger asChild>
226
- <div className="flex flex-shrink-0 items-center gap-1 text-green-500">
227
- <Server className="h-3 w-3" />
228
- <span className="text-xs">{runningDevServers.length}</span>
229
- </div>
230
- </TooltipTrigger>
231
- <TooltipContent>
232
- <p>
233
- {runningDevServers.length} dev server
234
- {runningDevServers.length > 1 ? "s" : ""} running
235
- </p>
236
- </TooltipContent>
237
- </Tooltip>
238
- )}
239
-
240
- {/* Session count */}
241
- <span className="text-muted-foreground flex-shrink-0 text-xs">
242
- {sessionCount}
243
- </span>
244
-
245
- {/* Actions menu */}
246
- {hasActions && (
247
- <DropdownMenu>
248
- <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
249
- <Button
250
- variant="ghost"
251
- size="icon-sm"
252
- className="h-7 w-7 flex-shrink-0 opacity-100 md:h-6 md:w-6 md:opacity-0 md:group-hover:opacity-100"
253
- >
254
- <MoreHorizontal className="h-4 w-4" />
255
- </Button>
256
- </DropdownMenuTrigger>
257
- <DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
258
- {renderMenuItems(false)}
259
- </DropdownMenuContent>
260
- </DropdownMenu>
261
- )}
262
- </div>
263
- );
264
-
265
- // Wrap with context menu if actions are available
266
- if (hasActions) {
267
- return (
268
- <ContextMenu>
269
- <ContextMenuTrigger asChild>{cardContent}</ContextMenuTrigger>
270
- <ContextMenuContent>{renderMenuItems(true)}</ContextMenuContent>
271
- </ContextMenu>
272
- );
273
- }
274
-
275
- return cardContent;
276
- }
@@ -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
- }