@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.
- 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/sessions/status/acknowledge/route.ts +8 -0
- package/app/api/sessions/status/route.ts +2 -233
- package/app/page.tsx +6 -13
- 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/DesktopTabBar.tsx +62 -28
- package/components/Pane/index.tsx +5 -0
- package/components/Projects/index.ts +0 -1
- package/components/QuickSwitcher.tsx +63 -11
- package/components/SessionList/ActiveSessionsSection.tsx +116 -0
- package/components/SessionList/hooks/useSessionListMutations.ts +0 -35
- package/components/SessionList/index.tsx +9 -1
- package/components/SessionStatusBar.tsx +155 -0
- package/components/WaitingBanner.tsx +122 -0
- package/components/views/DesktopView.tsx +27 -8
- package/components/views/MobileView.tsx +6 -1
- package/components/views/types.ts +2 -0
- package/data/sessions/index.ts +0 -1
- package/data/sessions/queries.ts +1 -27
- package/data/statuses/queries.ts +68 -34
- package/hooks/useSessions.ts +0 -12
- package/lib/claude/watcher.ts +28 -5
- package/lib/db/queries.ts +4 -64
- package/lib/db/types.ts +0 -8
- package/lib/hooks/reporter.ts +116 -0
- package/lib/hooks/setup.ts +164 -0
- package/lib/orchestration.ts +16 -23
- package/lib/providers/registry.ts +3 -57
- package/lib/providers.ts +19 -100
- package/lib/status-monitor.ts +303 -0
- package/package.json +1 -1
- package/server.ts +5 -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
- 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
|
-
}
|
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
|
-
}
|