@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,16 +1,14 @@
1
1
  import { useState, useEffect, useCallback } from "react";
2
- import { getProviderDefinition, type AgentType } from "@/lib/providers";
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
- agentType
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
- <div
108
- key={tab.id}
109
- onClick={(e) => {
110
- e.stopPropagation();
111
- onTabSwitch(tab.id);
112
- }}
113
- className={cn(
114
- "group flex cursor-pointer items-center gap-1.5 rounded-t-md px-3 py-1.5 text-xs transition-colors",
115
- tab.id === activeTabId
116
- ? "bg-background text-foreground"
117
- : "text-muted-foreground hover:text-foreground/80 hover:bg-accent/50"
118
- )}
119
- >
120
- <span className="max-w-[120px] truncate">{getTabName(tab)}</span>
121
- {tabs.length > 1 && (
122
- <button
123
- onClick={(e) => {
124
- e.stopPropagation();
125
- onTabClose(tab.id);
126
- }}
127
- className="hover:text-foreground ml-1 opacity-0 group-hover:opacity-100"
128
- >
129
- <X className="h-3 w-3" />
130
- </button>
131
- )}
132
- </div>
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}
@@ -1,3 +1,2 @@
1
- export { ProjectCard } from "./ProjectCard";
2
1
  export { NewProjectDialog } from "./NewProjectDialog";
3
2
  export { ProjectSettingsDialog } from "./ProjectSettingsDialog";
@@ -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
- if (!query) return allSessions;
108
- const q = query.toLowerCase();
109
- return allSessions.filter(
110
- (s) =>
111
- s.summary.toLowerCase().includes(q) ||
112
- s.projectDisplayName.toLowerCase().includes(q) ||
113
- s.cwd.toLowerCase().includes(q)
114
- );
115
- }, [allSessions, query]);
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: _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) => {