@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,16 +1,14 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import type { AgentType } from "@/lib/providers";
|
|
3
3
|
import type { ProjectWithDevServers } from "@/lib/projects";
|
|
4
4
|
import { setPendingPrompt } from "@/stores/initialPrompt";
|
|
5
5
|
import { useCreateSession } from "@/data/sessions";
|
|
6
6
|
import {
|
|
7
7
|
type GitInfo,
|
|
8
8
|
SKIP_PERMISSIONS_KEY,
|
|
9
|
-
AGENT_TYPE_KEY,
|
|
10
9
|
RECENT_DIRS_KEY,
|
|
11
10
|
USE_TMUX_KEY,
|
|
12
11
|
MAX_RECENT_DIRS,
|
|
13
|
-
AGENT_OPTIONS,
|
|
14
12
|
generateFeatureName,
|
|
15
13
|
} from "../NewSessionDialog.types";
|
|
16
14
|
|
|
@@ -42,7 +40,6 @@ export function useNewSessionForm({
|
|
|
42
40
|
const [name, setName] = useState("");
|
|
43
41
|
const [workingDirectory, setWorkingDirectory] = useState("~");
|
|
44
42
|
const [projectId, setProjectId] = useState<string | null>(null);
|
|
45
|
-
const [agentType, setAgentType] = useState<AgentType>("claude");
|
|
46
43
|
const [skipPermissions, setSkipPermissions] = useState(false);
|
|
47
44
|
const [useTmux, setUseTmux] = useState(true);
|
|
48
45
|
const [initialPrompt, setInitialPrompt] = useState("");
|
|
@@ -120,13 +117,6 @@ export function useNewSessionForm({
|
|
|
120
117
|
if (savedSkipPerms !== null) {
|
|
121
118
|
setSkipPermissions(savedSkipPerms === "true");
|
|
122
119
|
}
|
|
123
|
-
const savedAgentType = localStorage.getItem(AGENT_TYPE_KEY);
|
|
124
|
-
if (
|
|
125
|
-
savedAgentType &&
|
|
126
|
-
AGENT_OPTIONS.some((opt) => opt.value === savedAgentType)
|
|
127
|
-
) {
|
|
128
|
-
setAgentType(savedAgentType as AgentType);
|
|
129
|
-
}
|
|
130
120
|
const savedUseTmux = localStorage.getItem(USE_TMUX_KEY);
|
|
131
121
|
if (savedUseTmux !== null) {
|
|
132
122
|
setUseTmux(savedUseTmux === "true");
|
|
@@ -150,7 +140,6 @@ export function useNewSessionForm({
|
|
|
150
140
|
const project = projects.find((p) => p.id === selectedProjectId);
|
|
151
141
|
if (project && !project.is_uncategorized) {
|
|
152
142
|
setWorkingDirectory(project.working_directory);
|
|
153
|
-
setAgentType(project.agent_type);
|
|
154
143
|
}
|
|
155
144
|
} else {
|
|
156
145
|
// Otherwise, select first non-uncategorized project
|
|
@@ -158,25 +147,11 @@ export function useNewSessionForm({
|
|
|
158
147
|
if (firstProject) {
|
|
159
148
|
setProjectId(firstProject.id);
|
|
160
149
|
setWorkingDirectory(firstProject.working_directory);
|
|
161
|
-
setAgentType(firstProject.agent_type);
|
|
162
150
|
}
|
|
163
151
|
}
|
|
164
152
|
}
|
|
165
153
|
}, [open, selectedProjectId, projects]);
|
|
166
154
|
|
|
167
|
-
useEffect(() => {
|
|
168
|
-
if (!skipPermissions) {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (getProviderDefinition(agentType).autoApproveFlag) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
setSkipPermissions(false);
|
|
177
|
-
localStorage.setItem(SKIP_PERMISSIONS_KEY, "false");
|
|
178
|
-
}, [agentType, skipPermissions]);
|
|
179
|
-
|
|
180
155
|
// Save directory to recent list
|
|
181
156
|
const addRecentDirectory = useCallback((dir: string) => {
|
|
182
157
|
if (!dir || dir === "~") return;
|
|
@@ -196,7 +171,6 @@ export function useNewSessionForm({
|
|
|
196
171
|
const project = projects.find((p) => p.id === newProjectId);
|
|
197
172
|
if (project && !project.is_uncategorized) {
|
|
198
173
|
setWorkingDirectory(project.working_directory);
|
|
199
|
-
setAgentType(project.agent_type);
|
|
200
174
|
}
|
|
201
175
|
}
|
|
202
176
|
},
|
|
@@ -208,11 +182,6 @@ export function useNewSessionForm({
|
|
|
208
182
|
localStorage.setItem(SKIP_PERMISSIONS_KEY, String(checked));
|
|
209
183
|
};
|
|
210
184
|
|
|
211
|
-
const handleAgentTypeChange = (value: AgentType) => {
|
|
212
|
-
setAgentType(value);
|
|
213
|
-
localStorage.setItem(AGENT_TYPE_KEY, value);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
185
|
const handleUseTmuxChange = (checked: boolean) => {
|
|
217
186
|
setUseTmux(checked);
|
|
218
187
|
localStorage.setItem(USE_TMUX_KEY, String(checked));
|
|
@@ -248,7 +217,7 @@ export function useNewSessionForm({
|
|
|
248
217
|
name: name.trim() || undefined,
|
|
249
218
|
workingDirectory,
|
|
250
219
|
projectId,
|
|
251
|
-
agentType,
|
|
220
|
+
agentType: "claude" as AgentType,
|
|
252
221
|
useWorktree,
|
|
253
222
|
featureName: useWorktree ? featureName.trim() : null,
|
|
254
223
|
baseBranch: useWorktree ? baseBranch : null,
|
|
@@ -291,7 +260,7 @@ export function useNewSessionForm({
|
|
|
291
260
|
const newId = await onCreateProject(
|
|
292
261
|
newProjectName.trim(),
|
|
293
262
|
workingDirectory,
|
|
294
|
-
|
|
263
|
+
"claude"
|
|
295
264
|
);
|
|
296
265
|
if (newId) {
|
|
297
266
|
setProjectId(newId);
|
|
@@ -328,7 +297,6 @@ export function useNewSessionForm({
|
|
|
328
297
|
workingDirectory,
|
|
329
298
|
setWorkingDirectory,
|
|
330
299
|
projectId,
|
|
331
|
-
agentType,
|
|
332
300
|
skipPermissions,
|
|
333
301
|
useTmux,
|
|
334
302
|
initialPrompt,
|
|
@@ -361,7 +329,6 @@ export function useNewSessionForm({
|
|
|
361
329
|
// Handlers
|
|
362
330
|
handleProjectChange,
|
|
363
331
|
handleSkipPermissionsChange,
|
|
364
|
-
handleAgentTypeChange,
|
|
365
332
|
handleUseTmuxChange,
|
|
366
333
|
handleSubmit,
|
|
367
334
|
handleCreateProject,
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
import { Button } from "@/components/ui/button";
|
|
11
11
|
import { Input } from "@/components/ui/input";
|
|
12
12
|
import { useNewSessionForm } from "./hooks/useNewSessionForm";
|
|
13
|
-
import { AgentSelector } from "./AgentSelector";
|
|
14
13
|
import { CreatingOverlay } from "./CreatingOverlay";
|
|
15
14
|
import type { NewSessionDialogProps } from "./NewSessionDialog.types";
|
|
16
15
|
|
|
@@ -56,11 +55,6 @@ export function NewSessionDialog({
|
|
|
56
55
|
<DialogTitle>New Session</DialogTitle>
|
|
57
56
|
</DialogHeader>
|
|
58
57
|
<form onSubmit={form.handleSubmit} className="space-y-4">
|
|
59
|
-
<AgentSelector
|
|
60
|
-
value={form.agentType}
|
|
61
|
-
onChange={form.handleAgentTypeChange}
|
|
62
|
-
/>
|
|
63
|
-
|
|
64
58
|
<div className="space-y-2">
|
|
65
59
|
<label className="text-sm font-medium">
|
|
66
60
|
Name{" "}
|
|
@@ -100,7 +94,6 @@ export function NewSessionDialog({
|
|
|
100
94
|
</form>
|
|
101
95
|
</DialogContent>
|
|
102
96
|
</Dialog>
|
|
103
|
-
|
|
104
97
|
</>
|
|
105
98
|
);
|
|
106
99
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "@/components/ui/tooltip";
|
|
22
22
|
import { cn } from "@/lib/utils";
|
|
23
23
|
import type { Session } from "@/lib/db";
|
|
24
|
+
import type { SessionStatus } from "@/components/views/types";
|
|
24
25
|
|
|
25
26
|
type ViewMode = "terminal" | "files" | "git" | "workers";
|
|
26
27
|
|
|
@@ -35,6 +36,7 @@ interface DesktopTabBarProps {
|
|
|
35
36
|
activeTabId: string;
|
|
36
37
|
session: Session | null | undefined;
|
|
37
38
|
sessions: Session[];
|
|
39
|
+
sessionStatuses?: Record<string, SessionStatus>;
|
|
38
40
|
viewMode: ViewMode;
|
|
39
41
|
isFocused: boolean;
|
|
40
42
|
isConductor: boolean;
|
|
@@ -63,6 +65,7 @@ export function DesktopTabBar({
|
|
|
63
65
|
activeTabId,
|
|
64
66
|
session,
|
|
65
67
|
sessions,
|
|
68
|
+
sessionStatuses,
|
|
66
69
|
viewMode,
|
|
67
70
|
isFocused,
|
|
68
71
|
isConductor,
|
|
@@ -103,34 +106,65 @@ export function DesktopTabBar({
|
|
|
103
106
|
>
|
|
104
107
|
{/* Tabs */}
|
|
105
108
|
<div className="flex min-w-0 flex-1 items-center gap-0.5">
|
|
106
|
-
{tabs.map((tab) =>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
109
|
+
{tabs.map((tab) => {
|
|
110
|
+
const tabStatus = tab.sessionId
|
|
111
|
+
? sessionStatuses?.[tab.sessionId]
|
|
112
|
+
: undefined;
|
|
113
|
+
return (
|
|
114
|
+
<Tooltip key={tab.id}>
|
|
115
|
+
<TooltipTrigger asChild>
|
|
116
|
+
<div
|
|
117
|
+
onClick={(e) => {
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
onTabSwitch(tab.id);
|
|
120
|
+
}}
|
|
121
|
+
className={cn(
|
|
122
|
+
"group relative flex cursor-pointer items-center gap-1.5 rounded-t-md px-3 py-1.5 text-xs transition-colors",
|
|
123
|
+
tab.id === activeTabId
|
|
124
|
+
? "bg-background text-foreground"
|
|
125
|
+
: "text-muted-foreground hover:text-foreground/80 hover:bg-accent/50"
|
|
126
|
+
)}
|
|
127
|
+
>
|
|
128
|
+
{tabStatus &&
|
|
129
|
+
tab.id !== activeTabId &&
|
|
130
|
+
(tabStatus.status === "running" ||
|
|
131
|
+
tabStatus.status === "waiting") && (
|
|
132
|
+
<span
|
|
133
|
+
className={cn(
|
|
134
|
+
"absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full",
|
|
135
|
+
tabStatus.status === "running" &&
|
|
136
|
+
"animate-pulse bg-green-500",
|
|
137
|
+
tabStatus.status === "waiting" &&
|
|
138
|
+
"animate-pulse bg-amber-500"
|
|
139
|
+
)}
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
142
|
+
<span className="max-w-[120px] truncate">
|
|
143
|
+
{getTabName(tab)}
|
|
144
|
+
</span>
|
|
145
|
+
{tabs.length > 1 && (
|
|
146
|
+
<button
|
|
147
|
+
onClick={(e) => {
|
|
148
|
+
e.stopPropagation();
|
|
149
|
+
onTabClose(tab.id);
|
|
150
|
+
}}
|
|
151
|
+
className="hover:text-foreground ml-1 opacity-0 group-hover:opacity-100"
|
|
152
|
+
>
|
|
153
|
+
<X className="h-3 w-3" />
|
|
154
|
+
</button>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
</TooltipTrigger>
|
|
158
|
+
{tabStatus?.lastLine && tab.id !== activeTabId && (
|
|
159
|
+
<TooltipContent side="bottom" className="max-w-xs">
|
|
160
|
+
<p className="truncate font-mono text-xs">
|
|
161
|
+
{tabStatus.lastLine}
|
|
162
|
+
</p>
|
|
163
|
+
</TooltipContent>
|
|
164
|
+
)}
|
|
165
|
+
</Tooltip>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
134
168
|
<Tooltip>
|
|
135
169
|
<TooltipTrigger asChild>
|
|
136
170
|
<Button
|
|
@@ -43,10 +43,13 @@ const GitPanel = dynamic(
|
|
|
43
43
|
{ ssr: false, loading: () => <GitPanelSkeleton /> }
|
|
44
44
|
);
|
|
45
45
|
|
|
46
|
+
import type { SessionStatus } from "@/components/views/types";
|
|
47
|
+
|
|
46
48
|
interface PaneProps {
|
|
47
49
|
paneId: string;
|
|
48
50
|
sessions: Session[];
|
|
49
51
|
projects: Project[];
|
|
52
|
+
sessionStatuses?: Record<string, SessionStatus>;
|
|
50
53
|
onRegisterTerminal: (
|
|
51
54
|
paneId: string,
|
|
52
55
|
tabId: string,
|
|
@@ -68,6 +71,7 @@ export const Pane = memo(function Pane({
|
|
|
68
71
|
paneId,
|
|
69
72
|
sessions,
|
|
70
73
|
projects,
|
|
74
|
+
sessionStatuses,
|
|
71
75
|
onRegisterTerminal,
|
|
72
76
|
onMenuClick,
|
|
73
77
|
onSelectSession,
|
|
@@ -318,6 +322,7 @@ export const Pane = memo(function Pane({
|
|
|
318
322
|
activeTabId={paneData.activeTabId}
|
|
319
323
|
session={session}
|
|
320
324
|
sessions={sessions}
|
|
325
|
+
sessionStatuses={sessionStatuses}
|
|
321
326
|
viewMode={viewMode}
|
|
322
327
|
isFocused={isFocused}
|
|
323
328
|
isConductor={isConductor}
|
|
@@ -14,6 +14,7 @@ import { CodeSearchResults } from "@/components/CodeSearch/CodeSearchResults";
|
|
|
14
14
|
import { useRipgrepAvailable } from "@/data/code-search";
|
|
15
15
|
import { useClaudeProjectsQuery, useClaudeSessionsQuery } from "@/data/claude";
|
|
16
16
|
import type { ClaudeProject } from "@/data/claude";
|
|
17
|
+
import type { SessionStatus } from "@/components/views/types";
|
|
17
18
|
|
|
18
19
|
interface QuickSwitcherProps {
|
|
19
20
|
open: boolean;
|
|
@@ -27,6 +28,7 @@ interface QuickSwitcherProps {
|
|
|
27
28
|
onSelectFile?: (file: string, line: number) => void;
|
|
28
29
|
currentSessionId?: string;
|
|
29
30
|
activeSessionWorkingDir?: string;
|
|
31
|
+
sessionStatuses?: Record<string, SessionStatus>;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
interface FlatSession {
|
|
@@ -45,6 +47,7 @@ export function QuickSwitcher({
|
|
|
45
47
|
onSelectFile,
|
|
46
48
|
currentSessionId,
|
|
47
49
|
activeSessionWorkingDir,
|
|
50
|
+
sessionStatuses,
|
|
48
51
|
}: QuickSwitcherProps) {
|
|
49
52
|
const [mode, setMode] = useState<"sessions" | "code">("sessions");
|
|
50
53
|
const [query, setQuery] = useState("");
|
|
@@ -103,16 +106,46 @@ export function QuickSwitcher({
|
|
|
103
106
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- only re-run when .data changes, not entire query objects
|
|
104
107
|
}, [s0.data, s1.data, s2.data, s3.data, topProjects]);
|
|
105
108
|
|
|
109
|
+
// Build a map of claudeSessionId -> status for quick lookup
|
|
110
|
+
const statusByClaudeId = useMemo(() => {
|
|
111
|
+
if (!sessionStatuses) return new Map<string, SessionStatus>();
|
|
112
|
+
const map = new Map<string, SessionStatus>();
|
|
113
|
+
for (const s of Object.values(sessionStatuses)) {
|
|
114
|
+
if (s.claudeSessionId) {
|
|
115
|
+
map.set(s.claudeSessionId, s);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return map;
|
|
119
|
+
}, [sessionStatuses]);
|
|
120
|
+
|
|
106
121
|
const filteredSessions = useMemo(() => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
(
|
|
111
|
-
s
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
let sessions = allSessions;
|
|
123
|
+
if (query) {
|
|
124
|
+
const q = query.toLowerCase();
|
|
125
|
+
sessions = sessions.filter(
|
|
126
|
+
(s) =>
|
|
127
|
+
s.summary.toLowerCase().includes(q) ||
|
|
128
|
+
s.projectDisplayName.toLowerCase().includes(q) ||
|
|
129
|
+
s.cwd.toLowerCase().includes(q)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Sort: waiting first, then running, then by time
|
|
134
|
+
return [...sessions].sort((a, b) => {
|
|
135
|
+
const statusA = statusByClaudeId.get(a.sessionId)?.status;
|
|
136
|
+
const statusB = statusByClaudeId.get(b.sessionId)?.status;
|
|
137
|
+
const orderMap: Record<string, number> = {
|
|
138
|
+
waiting: 0,
|
|
139
|
+
running: 1,
|
|
140
|
+
};
|
|
141
|
+
const orderA = statusA && statusA in orderMap ? orderMap[statusA] : 2;
|
|
142
|
+
const orderB = statusB && statusB in orderMap ? orderMap[statusB] : 2;
|
|
143
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
144
|
+
return (
|
|
145
|
+
new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
}, [allSessions, query, statusByClaudeId]);
|
|
116
149
|
|
|
117
150
|
useEffect(() => {
|
|
118
151
|
if (open) {
|
|
@@ -242,6 +275,7 @@ export function QuickSwitcher({
|
|
|
242
275
|
) : (
|
|
243
276
|
filteredSessions.map((session, index) => {
|
|
244
277
|
const isCurrent = session.sessionId === currentSessionId;
|
|
278
|
+
const status = statusByClaudeId.get(session.sessionId);
|
|
245
279
|
return (
|
|
246
280
|
<button
|
|
247
281
|
key={session.sessionId}
|
|
@@ -259,11 +293,24 @@ export function QuickSwitcher({
|
|
|
259
293
|
index === selectedIndex
|
|
260
294
|
? "bg-accent"
|
|
261
295
|
: "hover:bg-accent/50",
|
|
262
|
-
isCurrent && "bg-primary/10"
|
|
296
|
+
isCurrent && "bg-primary/10",
|
|
297
|
+
status?.status === "waiting" && "bg-amber-500/5"
|
|
263
298
|
)}
|
|
264
299
|
>
|
|
265
|
-
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md bg-emerald-500/20 text-emerald-400">
|
|
300
|
+
<div className="relative flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md bg-emerald-500/20 text-emerald-400">
|
|
266
301
|
<Terminal className="h-4 w-4" />
|
|
302
|
+
{status && (
|
|
303
|
+
<span
|
|
304
|
+
className={cn(
|
|
305
|
+
"border-background absolute -top-0.5 -right-0.5 h-2.5 w-2.5 rounded-full border-2",
|
|
306
|
+
status.status === "running" &&
|
|
307
|
+
"animate-pulse bg-green-500",
|
|
308
|
+
status.status === "waiting" &&
|
|
309
|
+
"animate-pulse bg-amber-500",
|
|
310
|
+
status.status === "idle" && "bg-gray-400"
|
|
311
|
+
)}
|
|
312
|
+
/>
|
|
313
|
+
)}
|
|
267
314
|
</div>
|
|
268
315
|
<div className="min-w-0 flex-1">
|
|
269
316
|
<span className="block truncate text-sm font-medium">
|
|
@@ -272,6 +319,11 @@ export function QuickSwitcher({
|
|
|
272
319
|
<span className="text-muted-foreground block truncate text-xs">
|
|
273
320
|
{session.projectDisplayName}
|
|
274
321
|
</span>
|
|
322
|
+
{status?.lastLine && (
|
|
323
|
+
<span className="text-muted-foreground block truncate font-mono text-[10px]">
|
|
324
|
+
{status.lastLine}
|
|
325
|
+
</span>
|
|
326
|
+
)}
|
|
275
327
|
</div>
|
|
276
328
|
<div className="text-muted-foreground flex flex-shrink-0 items-center gap-1 text-xs">
|
|
277
329
|
<Clock className="h-3 w-3" />
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState, useEffect } from "react";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
import { ChevronRight, Activity, AlertCircle, Moon } from "lucide-react";
|
|
6
|
+
import type { SessionStatus } from "@/components/views/types";
|
|
7
|
+
|
|
8
|
+
interface ActiveSessionsSectionProps {
|
|
9
|
+
sessionStatuses: Record<string, SessionStatus>;
|
|
10
|
+
onSelect: (sessionId: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const STATUS_ORDER: Record<string, number> = {
|
|
14
|
+
waiting: 0,
|
|
15
|
+
running: 1,
|
|
16
|
+
idle: 2,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function ActiveSessionsSection({
|
|
20
|
+
sessionStatuses,
|
|
21
|
+
onSelect,
|
|
22
|
+
}: ActiveSessionsSectionProps) {
|
|
23
|
+
const activeSessions = useMemo(() => {
|
|
24
|
+
return Object.entries(sessionStatuses)
|
|
25
|
+
.filter(
|
|
26
|
+
([, s]) =>
|
|
27
|
+
s.status === "running" ||
|
|
28
|
+
s.status === "waiting" ||
|
|
29
|
+
s.status === "idle"
|
|
30
|
+
)
|
|
31
|
+
.map(([id, s]) => ({ id, ...s }))
|
|
32
|
+
.sort(
|
|
33
|
+
(a, b) => (STATUS_ORDER[a.status] ?? 3) - (STATUS_ORDER[b.status] ?? 3)
|
|
34
|
+
);
|
|
35
|
+
}, [sessionStatuses]);
|
|
36
|
+
|
|
37
|
+
const hasWaiting = activeSessions.some((s) => s.status === "waiting");
|
|
38
|
+
const [expanded, setExpanded] = useState(hasWaiting);
|
|
39
|
+
|
|
40
|
+
// Auto-expand when a session starts waiting
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (hasWaiting) setExpanded(true);
|
|
43
|
+
}, [hasWaiting]);
|
|
44
|
+
|
|
45
|
+
if (activeSessions.length === 0) return null;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="mb-1">
|
|
49
|
+
<button
|
|
50
|
+
onClick={() => setExpanded((prev) => !prev)}
|
|
51
|
+
className={cn(
|
|
52
|
+
"flex w-full items-center gap-2 px-3 py-1.5 text-xs font-medium transition-colors",
|
|
53
|
+
hasWaiting
|
|
54
|
+
? "text-amber-500"
|
|
55
|
+
: "text-muted-foreground hover:text-foreground"
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
<ChevronRight
|
|
59
|
+
className={cn(
|
|
60
|
+
"h-3 w-3 transition-transform",
|
|
61
|
+
expanded && "rotate-90"
|
|
62
|
+
)}
|
|
63
|
+
/>
|
|
64
|
+
<span>Active Sessions</span>
|
|
65
|
+
<span
|
|
66
|
+
className={cn(
|
|
67
|
+
"ml-auto rounded-full px-1.5 py-0.5 text-[10px]",
|
|
68
|
+
hasWaiting
|
|
69
|
+
? "bg-amber-500/20 text-amber-500"
|
|
70
|
+
: "bg-muted text-muted-foreground"
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
{activeSessions.length}
|
|
74
|
+
</span>
|
|
75
|
+
</button>
|
|
76
|
+
|
|
77
|
+
{expanded && (
|
|
78
|
+
<div className="space-y-0.5 px-1.5">
|
|
79
|
+
{activeSessions.map((session) => (
|
|
80
|
+
<button
|
|
81
|
+
key={session.id}
|
|
82
|
+
onClick={() => onSelect(session.id)}
|
|
83
|
+
className="hover:bg-accent group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors"
|
|
84
|
+
>
|
|
85
|
+
<StatusIcon status={session.status} />
|
|
86
|
+
<div className="min-w-0 flex-1">
|
|
87
|
+
<span className="block truncate text-xs font-medium">
|
|
88
|
+
{session.sessionName}
|
|
89
|
+
</span>
|
|
90
|
+
{session.lastLine && (
|
|
91
|
+
<span className="text-muted-foreground block truncate font-mono text-[10px]">
|
|
92
|
+
{session.lastLine}
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
</button>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function StatusIcon({ status }: { status: string }) {
|
|
105
|
+
if (status === "running") {
|
|
106
|
+
return (
|
|
107
|
+
<Activity className="h-3 w-3 flex-shrink-0 animate-pulse text-green-500" />
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (status === "waiting") {
|
|
111
|
+
return (
|
|
112
|
+
<AlertCircle className="h-3 w-3 flex-shrink-0 animate-pulse text-amber-500" />
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return <Moon className="h-3 w-3 flex-shrink-0 text-gray-400" />;
|
|
116
|
+
}
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
useDeleteProject,
|
|
14
14
|
useRenameProject,
|
|
15
15
|
} from "@/data/projects";
|
|
16
|
-
import { useToggleGroup, useCreateGroup, useDeleteGroup } from "@/data/groups";
|
|
17
16
|
import {
|
|
18
17
|
useStopDevServer,
|
|
19
18
|
useRestartDevServer,
|
|
@@ -42,11 +41,6 @@ export function useSessionListMutations({
|
|
|
42
41
|
const deleteProjectMutation = useDeleteProject();
|
|
43
42
|
const renameProjectMutation = useRenameProject();
|
|
44
43
|
|
|
45
|
-
// Group mutations
|
|
46
|
-
const toggleGroupMutation = useToggleGroup();
|
|
47
|
-
const createGroupMutation = useCreateGroup();
|
|
48
|
-
const deleteGroupMutation = useDeleteGroup();
|
|
49
|
-
|
|
50
44
|
// Dev server mutations
|
|
51
45
|
const stopDevServerMutation = useStopDevServer();
|
|
52
46
|
const restartDevServerMutation = useRestartDevServer();
|
|
@@ -124,30 +118,6 @@ export function useSessionListMutations({
|
|
|
124
118
|
[renameProjectMutation]
|
|
125
119
|
);
|
|
126
120
|
|
|
127
|
-
// Group handlers
|
|
128
|
-
const handleToggleGroup = useCallback(
|
|
129
|
-
async (path: string, expanded: boolean) => {
|
|
130
|
-
await toggleGroupMutation.mutateAsync({ path, expanded });
|
|
131
|
-
},
|
|
132
|
-
[toggleGroupMutation]
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
const handleCreateGroup = useCallback(
|
|
136
|
-
async (name: string, parentPath?: string) => {
|
|
137
|
-
await createGroupMutation.mutateAsync({ name, parentPath });
|
|
138
|
-
},
|
|
139
|
-
[createGroupMutation]
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
const handleDeleteGroup = useCallback(
|
|
143
|
-
async (path: string) => {
|
|
144
|
-
if (!confirm("Delete this group? Sessions will be moved to parent."))
|
|
145
|
-
return;
|
|
146
|
-
await deleteGroupMutation.mutateAsync(path);
|
|
147
|
-
},
|
|
148
|
-
[deleteGroupMutation]
|
|
149
|
-
);
|
|
150
|
-
|
|
151
121
|
// Dev server handlers
|
|
152
122
|
const handleStopDevServer = useCallback(
|
|
153
123
|
async (serverId: string) => {
|
|
@@ -252,11 +222,6 @@ export function useSessionListMutations({
|
|
|
252
222
|
handleDeleteProject,
|
|
253
223
|
handleRenameProject,
|
|
254
224
|
|
|
255
|
-
// Group handlers
|
|
256
|
-
handleToggleGroup,
|
|
257
|
-
handleCreateGroup,
|
|
258
|
-
handleDeleteGroup,
|
|
259
|
-
|
|
260
225
|
// Dev server handlers
|
|
261
226
|
handleStopDevServer,
|
|
262
227
|
handleRestartDevServer,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useRef, useCallback } from "react";
|
|
4
4
|
import { ClaudeProjectsSection } from "@/components/ClaudeProjects";
|
|
5
|
+
import { ActiveSessionsSection } from "./ActiveSessionsSection";
|
|
5
6
|
import { NewProjectDialog } from "@/components/Projects";
|
|
6
7
|
import { FolderPicker } from "@/components/FolderPicker";
|
|
7
8
|
import { SelectionToolbar } from "./SelectionToolbar";
|
|
@@ -24,7 +25,7 @@ export type { SessionListProps } from "./SessionList.types";
|
|
|
24
25
|
|
|
25
26
|
export function SessionList({
|
|
26
27
|
activeSessionId: _activeSessionId,
|
|
27
|
-
sessionStatuses
|
|
28
|
+
sessionStatuses,
|
|
28
29
|
onSelect,
|
|
29
30
|
onOpenInTab: _onOpenInTab,
|
|
30
31
|
onNewSessionInProject: _onNewSessionInProject,
|
|
@@ -137,6 +138,13 @@ export function SessionList({
|
|
|
137
138
|
</div>
|
|
138
139
|
)}
|
|
139
140
|
|
|
141
|
+
{!isInitialLoading && !hasError && sessionStatuses && (
|
|
142
|
+
<ActiveSessionsSection
|
|
143
|
+
sessionStatuses={sessionStatuses}
|
|
144
|
+
onSelect={onSelect}
|
|
145
|
+
/>
|
|
146
|
+
)}
|
|
147
|
+
|
|
140
148
|
{!isInitialLoading && !hasError && (
|
|
141
149
|
<ClaudeProjectsSection
|
|
142
150
|
onSelectSession={(claudeSessionId, cwd, summary, projectName) => {
|