@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
@@ -0,0 +1,155 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState, useRef, useEffect, useCallback } from "react";
4
+ import { cn } from "@/lib/utils";
5
+ import type { SessionStatus } from "@/components/views/types";
6
+ import { Activity, AlertCircle, Moon, ChevronUp } from "lucide-react";
7
+
8
+ interface SessionStatusBarProps {
9
+ sessionStatuses: Record<string, SessionStatus>;
10
+ onSelectSession: (sessionId: string) => void;
11
+ }
12
+
13
+ type StatusFilter = "running" | "waiting" | "idle" | null;
14
+
15
+ export function SessionStatusBar({
16
+ sessionStatuses,
17
+ onSelectSession,
18
+ }: SessionStatusBarProps) {
19
+ const [expandedFilter, setExpandedFilter] = useState<StatusFilter>(null);
20
+ const panelRef = useRef<HTMLDivElement>(null);
21
+
22
+ const counts = useMemo(() => {
23
+ const values = Object.values(sessionStatuses);
24
+ return {
25
+ running: values.filter((s) => s.status === "running").length,
26
+ waiting: values.filter((s) => s.status === "waiting").length,
27
+ idle: values.filter((s) => s.status === "idle").length,
28
+ };
29
+ }, [sessionStatuses]);
30
+
31
+ const filteredSessions = useMemo(() => {
32
+ if (!expandedFilter) return [];
33
+ return Object.entries(sessionStatuses)
34
+ .filter(([, s]) => s.status === expandedFilter)
35
+ .map(([id, s]) => ({ id, ...s }));
36
+ }, [sessionStatuses, expandedFilter]);
37
+
38
+ const handleToggle = useCallback((filter: StatusFilter) => {
39
+ setExpandedFilter((prev) => (prev === filter ? null : filter));
40
+ }, []);
41
+
42
+ // Close panel on outside click
43
+ useEffect(() => {
44
+ if (!expandedFilter) return;
45
+ const handler = (e: MouseEvent) => {
46
+ if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
47
+ setExpandedFilter(null);
48
+ }
49
+ };
50
+ document.addEventListener("mousedown", handler);
51
+ return () => document.removeEventListener("mousedown", handler);
52
+ }, [expandedFilter]);
53
+
54
+ const total = counts.running + counts.waiting + counts.idle;
55
+ if (total === 0) return null;
56
+
57
+ return (
58
+ <div ref={panelRef} className="relative flex-shrink-0">
59
+ {/* Expanded panel */}
60
+ {expandedFilter && filteredSessions.length > 0 && (
61
+ <div className="border-border bg-background absolute right-0 bottom-full left-0 z-10 max-h-48 overflow-y-auto border-t shadow-lg">
62
+ {filteredSessions.map((session) => (
63
+ <button
64
+ key={session.id}
65
+ onClick={() => {
66
+ onSelectSession(session.id);
67
+ setExpandedFilter(null);
68
+ }}
69
+ className="hover:bg-accent flex w-full items-center gap-3 px-4 py-2 text-left text-sm transition-colors"
70
+ >
71
+ <StatusDot status={session.status} />
72
+ <span className="min-w-0 flex-1 truncate font-medium">
73
+ {session.sessionName}
74
+ </span>
75
+ {session.lastLine && (
76
+ <span className="text-muted-foreground max-w-[40%] truncate font-mono text-xs">
77
+ {session.lastLine}
78
+ </span>
79
+ )}
80
+ </button>
81
+ ))}
82
+ </div>
83
+ )}
84
+
85
+ {/* Status bar */}
86
+ <div
87
+ className={cn(
88
+ "border-border flex h-8 items-center gap-4 border-t px-4 text-xs",
89
+ counts.waiting > 0 && "bg-amber-500/5"
90
+ )}
91
+ >
92
+ {counts.running > 0 && (
93
+ <button
94
+ onClick={() => handleToggle("running")}
95
+ className={cn(
96
+ "flex items-center gap-1.5 transition-colors",
97
+ expandedFilter === "running"
98
+ ? "text-foreground"
99
+ : "text-muted-foreground hover:text-foreground"
100
+ )}
101
+ >
102
+ <Activity className="h-3 w-3 text-green-500" />
103
+ <span>{counts.running} running</span>
104
+ {expandedFilter === "running" && <ChevronUp className="h-3 w-3" />}
105
+ </button>
106
+ )}
107
+
108
+ {counts.waiting > 0 && (
109
+ <button
110
+ onClick={() => handleToggle("waiting")}
111
+ className={cn(
112
+ "flex items-center gap-1.5 transition-colors",
113
+ expandedFilter === "waiting"
114
+ ? "text-foreground"
115
+ : "text-amber-500 hover:text-amber-400"
116
+ )}
117
+ >
118
+ <AlertCircle className="h-3 w-3 animate-pulse" />
119
+ <span>{counts.waiting} waiting</span>
120
+ {expandedFilter === "waiting" && <ChevronUp className="h-3 w-3" />}
121
+ </button>
122
+ )}
123
+
124
+ {counts.idle > 0 && (
125
+ <button
126
+ onClick={() => handleToggle("idle")}
127
+ className={cn(
128
+ "flex items-center gap-1.5 transition-colors",
129
+ expandedFilter === "idle"
130
+ ? "text-foreground"
131
+ : "text-muted-foreground hover:text-foreground"
132
+ )}
133
+ >
134
+ <Moon className="h-3 w-3" />
135
+ <span>{counts.idle} idle</span>
136
+ {expandedFilter === "idle" && <ChevronUp className="h-3 w-3" />}
137
+ </button>
138
+ )}
139
+ </div>
140
+ </div>
141
+ );
142
+ }
143
+
144
+ function StatusDot({ status }: { status: string }) {
145
+ return (
146
+ <span
147
+ className={cn(
148
+ "inline-block h-2 w-2 flex-shrink-0 rounded-full",
149
+ status === "running" && "animate-pulse bg-green-500",
150
+ status === "waiting" && "animate-pulse bg-amber-500",
151
+ status === "idle" && "bg-gray-400"
152
+ )}
153
+ />
154
+ );
155
+ }
@@ -0,0 +1,122 @@
1
+ "use client";
2
+
3
+ import { useMemo, useCallback, useState } from "react";
4
+ import { cn } from "@/lib/utils";
5
+ import { AlertCircle, ArrowRight, X } from "lucide-react";
6
+ import { Button } from "@/components/ui/button";
7
+ import type { SessionStatus } from "@/components/views/types";
8
+
9
+ interface WaitingBannerProps {
10
+ sessionStatuses: Record<string, SessionStatus>;
11
+ onSelectSession: (sessionId: string) => void;
12
+ }
13
+
14
+ const MAX_VISIBLE = 3;
15
+
16
+ export function WaitingBanner({
17
+ sessionStatuses,
18
+ onSelectSession,
19
+ }: WaitingBannerProps) {
20
+ const [dismissed, setDismissed] = useState<Set<string>>(new Set());
21
+
22
+ const waitingSessions = useMemo(() => {
23
+ const sessions = Object.entries(sessionStatuses)
24
+ .filter(([, s]) => s.status === "waiting")
25
+ .map(([id, s]) => ({ id, ...s }));
26
+
27
+ // Clear dismissed entries that are no longer waiting
28
+ const waitingIds = new Set(sessions.map((s) => s.id));
29
+ setDismissed((prev) => {
30
+ const next = new Set([...prev].filter((id) => waitingIds.has(id)));
31
+ return next.size === prev.size ? prev : next;
32
+ });
33
+
34
+ return sessions;
35
+ }, [sessionStatuses]);
36
+
37
+ const visibleSessions = useMemo(
38
+ () => waitingSessions.filter((s) => !dismissed.has(s.id)),
39
+ [waitingSessions, dismissed]
40
+ );
41
+
42
+ const handleDismiss = useCallback((sessionId: string) => {
43
+ setDismissed((prev) => new Set([...prev, sessionId]));
44
+ }, []);
45
+
46
+ const handleDismissAll = useCallback(() => {
47
+ setDismissed(new Set(waitingSessions.map((s) => s.id)));
48
+ }, [waitingSessions]);
49
+
50
+ if (visibleSessions.length === 0) return null;
51
+
52
+ const visible = visibleSessions.slice(0, MAX_VISIBLE);
53
+ const overflow = visibleSessions.length - MAX_VISIBLE;
54
+
55
+ return (
56
+ <div className="flex-shrink-0 border-b border-amber-500/30 bg-amber-500/5">
57
+ <div className="space-y-0">
58
+ {visible.map((session) => (
59
+ <div
60
+ key={session.id}
61
+ className={cn(
62
+ "flex items-center gap-3 px-4 py-2",
63
+ "border-l-2 border-amber-500"
64
+ )}
65
+ >
66
+ <AlertCircle className="h-4 w-4 flex-shrink-0 animate-pulse text-amber-500" />
67
+ <div className="min-w-0 flex-1">
68
+ <span className="text-sm font-medium">{session.sessionName}</span>
69
+ {session.waitingContext ? (
70
+ <pre className="text-muted-foreground mt-0.5 max-w-full truncate font-mono text-xs">
71
+ {session.waitingContext.split("\n").pop()}
72
+ </pre>
73
+ ) : session.lastLine ? (
74
+ <pre className="text-muted-foreground mt-0.5 max-w-full truncate font-mono text-xs">
75
+ {session.lastLine}
76
+ </pre>
77
+ ) : null}
78
+ </div>
79
+ <div className="flex items-center gap-1">
80
+ <Button
81
+ variant="ghost"
82
+ size="sm"
83
+ className="h-7 gap-1 text-amber-600 hover:text-amber-500"
84
+ onClick={() => onSelectSession(session.id)}
85
+ >
86
+ Switch
87
+ <ArrowRight className="h-3 w-3" />
88
+ </Button>
89
+ <Button
90
+ variant="ghost"
91
+ size="icon-sm"
92
+ className="text-muted-foreground h-6 w-6"
93
+ onClick={() => handleDismiss(session.id)}
94
+ >
95
+ <X className="h-3 w-3" />
96
+ </Button>
97
+ </div>
98
+ </div>
99
+ ))}
100
+
101
+ {overflow > 0 && (
102
+ <div className="text-muted-foreground px-4 py-1 text-xs">
103
+ +{overflow} more session{overflow > 1 ? "s" : ""} waiting
104
+ </div>
105
+ )}
106
+
107
+ {visibleSessions.length > 1 && (
108
+ <div className="flex justify-end px-4 py-1">
109
+ <Button
110
+ variant="ghost"
111
+ size="sm"
112
+ className="text-muted-foreground h-6 text-xs"
113
+ onClick={handleDismissAll}
114
+ >
115
+ Dismiss all
116
+ </Button>
117
+ </div>
118
+ )}
119
+ </div>
120
+ </div>
121
+ );
122
+ }
@@ -15,6 +15,8 @@ import {
15
15
  Command,
16
16
  } from "lucide-react";
17
17
  import { PaneLayout } from "@/components/PaneLayout";
18
+ import { SessionStatusBar } from "@/components/SessionStatusBar";
19
+ import { WaitingBanner } from "@/components/WaitingBanner";
18
20
  import {
19
21
  Tooltip,
20
22
  TooltipContent,
@@ -59,6 +61,16 @@ export function DesktopView({
59
61
  resumeClaudeSession,
60
62
  renderPane,
61
63
  }: ViewProps) {
64
+ const selectSessionById = (id: string) => {
65
+ const session = sessions.find((s) => s.id === id);
66
+ if (session) {
67
+ attachToSession(session);
68
+ return;
69
+ }
70
+ const status = sessionStatuses[id];
71
+ resumeClaudeSession(id, status?.cwd || "~");
72
+ };
73
+
62
74
  return (
63
75
  <div className="bg-background flex h-screen overflow-hidden">
64
76
  {/* Desktop Sidebar */}
@@ -71,10 +83,7 @@ export function DesktopView({
71
83
  <SessionList
72
84
  activeSessionId={focusedActiveTab?.sessionId || undefined}
73
85
  sessionStatuses={sessionStatuses}
74
- onSelect={(id) => {
75
- const session = sessions.find((s) => s.id === id);
76
- if (session) attachToSession(session);
77
- }}
86
+ onSelect={selectSessionById}
78
87
  onOpenInTab={(id) => {
79
88
  const session = sessions.find((s) => s.id === id);
80
89
  if (session) openSessionInNewTab(session);
@@ -191,10 +200,7 @@ export function DesktopView({
191
200
  .map((s) => ({ id: s.id, name: s.name }))}
192
201
  onUpdateSettings={updateSettings}
193
202
  onRequestPermission={requestPermission}
194
- onSelectSession={(id) => {
195
- const session = sessions.find((s) => s.id === id);
196
- if (session) attachToSession(session);
197
- }}
203
+ onSelectSession={selectSessionById}
198
204
  />
199
205
  <Button size="sm" onClick={() => newClaudeSession()}>
200
206
  <Plus className="mr-1 h-4 w-4" />
@@ -203,10 +209,22 @@ export function DesktopView({
203
209
  </div>
204
210
  </header>
205
211
 
212
+ {/* Waiting Banner */}
213
+ <WaitingBanner
214
+ sessionStatuses={sessionStatuses}
215
+ onSelectSession={selectSessionById}
216
+ />
217
+
206
218
  {/* Pane Layout - full height */}
207
219
  <div className="min-h-0 flex-1">
208
220
  <PaneLayout renderPane={renderPane} />
209
221
  </div>
222
+
223
+ {/* Session Status Bar */}
224
+ <SessionStatusBar
225
+ sessionStatuses={sessionStatuses}
226
+ onSelectSession={selectSessionById}
227
+ />
210
228
  </div>
211
229
 
212
230
  {/* Dialogs */}
@@ -223,6 +241,7 @@ export function DesktopView({
223
241
  onOpenChange={setShowQuickSwitcher}
224
242
  currentSessionId={focusedActiveTab?.sessionId ?? undefined}
225
243
  activeSessionWorkingDir={activeSession?.working_directory ?? undefined}
244
+ sessionStatuses={sessionStatuses}
226
245
  onResumeClaudeSession={resumeClaudeSession}
227
246
  onSelectFile={(file, line) => {
228
247
  const absolutePath = activeSession?.working_directory
@@ -49,7 +49,12 @@ export function MobileView({
49
49
  sessionStatuses={sessionStatuses}
50
50
  onSelect={(id) => {
51
51
  const session = sessions.find((s) => s.id === id);
52
- if (session) attachToSession(session);
52
+ if (session) {
53
+ attachToSession(session);
54
+ } else {
55
+ const status = sessionStatuses[id];
56
+ resumeClaudeSession(id, status?.cwd || "~");
57
+ }
53
58
  setSidebarOpen(false);
54
59
  }}
55
60
  onOpenInTab={(id) => {
@@ -5,8 +5,10 @@ import type { TabData } from "@/lib/panes";
5
5
 
6
6
  export interface SessionStatus {
7
7
  sessionName: string;
8
+ cwd?: string | null;
8
9
  status: "idle" | "running" | "waiting" | "error" | "dead";
9
10
  lastLine?: string;
11
+ waitingContext?: string;
10
12
  claudeSessionId?: string | null;
11
13
  }
12
14
 
@@ -6,7 +6,6 @@ export {
6
6
  useRenameSession,
7
7
  useForkSession,
8
8
  useSummarizeSession,
9
- useMoveSessionToGroup,
10
9
  useMoveSessionToProject,
11
10
  } from "./queries";
12
11
  export type { CreateSessionInput } from "./queries";
@@ -1,11 +1,10 @@
1
1
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
- import type { Session, Group } from "@/lib/db";
2
+ import type { Session } from "@/lib/db";
3
3
  import type { AgentType } from "@/lib/providers";
4
4
  import { sessionKeys } from "./keys";
5
5
 
6
6
  interface SessionsResponse {
7
7
  sessions: Session[];
8
- groups: Group[];
9
8
  }
10
9
 
11
10
  async function fetchSessions(): Promise<SessionsResponse> {
@@ -124,31 +123,6 @@ export function useSummarizeSession() {
124
123
  });
125
124
  }
126
125
 
127
- export function useMoveSessionToGroup() {
128
- const queryClient = useQueryClient();
129
-
130
- return useMutation({
131
- mutationFn: async ({
132
- sessionId,
133
- groupPath,
134
- }: {
135
- sessionId: string;
136
- groupPath: string;
137
- }) => {
138
- const res = await fetch(`/api/sessions/${sessionId}`, {
139
- method: "PATCH",
140
- headers: { "Content-Type": "application/json" },
141
- body: JSON.stringify({ groupPath }),
142
- });
143
- if (!res.ok) throw new Error("Failed to move session");
144
- return res.json();
145
- },
146
- onSuccess: () => {
147
- queryClient.invalidateQueries({ queryKey: sessionKeys.list() });
148
- },
149
- });
150
- }
151
-
152
126
  export function useMoveSessionToProject() {
153
127
  const queryClient = useQueryClient();
154
128
 
@@ -1,18 +1,6 @@
1
- import { useQuery } from "@tanstack/react-query";
2
- import { useEffect } from "react";
1
+ import { useEffect, useRef, useState, useCallback } from "react";
3
2
  import type { Session } from "@/lib/db";
4
3
  import type { SessionStatus } from "@/components/views/types";
5
- import { statusKeys } from "../sessions/keys";
6
-
7
- interface StatusResponse {
8
- statuses: Record<string, SessionStatus>;
9
- }
10
-
11
- async function fetchStatuses(): Promise<StatusResponse> {
12
- const res = await fetch("/api/sessions/status");
13
- if (!res.ok) throw new Error("Failed to fetch statuses");
14
- return res.json();
15
- }
16
4
 
17
5
  interface UseSessionStatusesOptions {
18
6
  sessions: Session[];
@@ -32,38 +20,84 @@ export function useSessionStatusesQuery({
32
20
  activeSessionId,
33
21
  checkStateChanges,
34
22
  }: UseSessionStatusesOptions) {
35
- const query = useQuery({
36
- queryKey: statusKeys.all,
37
- queryFn: fetchStatuses,
38
- staleTime: 2000,
39
- refetchInterval: (query) => {
40
- const statuses = query.state.data?.statuses;
41
- if (!statuses) return 5000;
23
+ const [sessionStatuses, setSessionStatuses] = useState<
24
+ Record<string, SessionStatus>
25
+ >({});
26
+ const wsRef = useRef<WebSocket | null>(null);
27
+ const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
28
+ null
29
+ );
42
30
 
43
- const hasActive = Object.values(statuses).some(
44
- (s) => s.status === "running" || s.status === "waiting"
45
- );
46
-
47
- return hasActive ? 5000 : 30000;
31
+ const handleStatuses = useCallback(
32
+ (statuses: Record<string, SessionStatus>) => {
33
+ setSessionStatuses(statuses);
48
34
  },
49
- });
35
+ []
36
+ );
50
37
 
38
+ // Check state changes when statuses or sessions update
51
39
  useEffect(() => {
52
- if (!query.data?.statuses) return;
53
-
54
- const statuses = query.data.statuses;
40
+ if (Object.keys(sessionStatuses).length === 0) return;
55
41
 
56
42
  const sessionStates = sessions.map((s) => ({
57
43
  id: s.id,
58
44
  name: s.name,
59
- status: (statuses[s.id]?.status || "dead") as SessionStatus["status"],
45
+ status: (sessionStatuses[s.id]?.status ||
46
+ "dead") as SessionStatus["status"],
60
47
  }));
61
48
  checkStateChanges(sessionStates, activeSessionId);
62
- // Note: claude_session_id is now updated server-side in /api/sessions/status
63
- }, [query.data, sessions, activeSessionId, checkStateChanges]);
49
+ }, [sessionStatuses, sessions, activeSessionId, checkStateChanges]);
50
+
51
+ // WebSocket connection for push-based updates
52
+ useEffect(() => {
53
+ let disposed = false;
54
+
55
+ function connect() {
56
+ if (disposed) return;
57
+
58
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
59
+ const ws = new WebSocket(
60
+ `${protocol}//${window.location.host}/ws/updates`
61
+ );
62
+ wsRef.current = ws;
63
+
64
+ ws.onmessage = (event) => {
65
+ try {
66
+ const msg = JSON.parse(event.data);
67
+ if (msg.type === "session-statuses") {
68
+ handleStatuses(msg.statuses);
69
+ }
70
+ } catch {
71
+ // ignore parse errors
72
+ }
73
+ };
74
+
75
+ ws.onclose = () => {
76
+ wsRef.current = null;
77
+ if (!disposed) {
78
+ // Reconnect after 2s
79
+ reconnectTimeoutRef.current = setTimeout(connect, 2000);
80
+ }
81
+ };
82
+
83
+ ws.onerror = () => {
84
+ ws.close();
85
+ };
86
+ }
87
+
88
+ connect();
89
+
90
+ return () => {
91
+ disposed = true;
92
+ if (reconnectTimeoutRef.current) {
93
+ clearTimeout(reconnectTimeoutRef.current);
94
+ }
95
+ wsRef.current?.close();
96
+ };
97
+ }, [handleStatuses]);
64
98
 
65
99
  return {
66
- sessionStatuses: query.data?.statuses ?? {},
67
- isLoading: query.isLoading,
100
+ sessionStatuses,
101
+ isLoading: false,
68
102
  };
69
103
  }
@@ -6,20 +6,17 @@ import {
6
6
  useRenameSession,
7
7
  useForkSession,
8
8
  useSummarizeSession,
9
- useMoveSessionToGroup,
10
9
  useMoveSessionToProject,
11
10
  } from "@/data/sessions";
12
11
 
13
12
  export function useSessions() {
14
13
  const { data, refetch } = useSessionsQuery();
15
14
  const sessions = data?.sessions ?? [];
16
- const groups = data?.groups ?? [];
17
15
 
18
16
  const deleteMutation = useDeleteSession();
19
17
  const renameMutation = useRenameSession();
20
18
  const forkMutation = useForkSession();
21
19
  const summarizeMutation = useSummarizeSession();
22
- const moveToGroupMutation = useMoveSessionToGroup();
23
20
  const moveToProjectMutation = useMoveSessionToProject();
24
21
 
25
22
  const fetchSessions = useCallback(async () => {
@@ -55,13 +52,6 @@ export function useSessions() {
55
52
  [summarizeMutation]
56
53
  );
57
54
 
58
- const moveSessionToGroup = useCallback(
59
- async (sessionId: string, groupPath: string) => {
60
- await moveToGroupMutation.mutateAsync({ sessionId, groupPath });
61
- },
62
- [moveToGroupMutation]
63
- );
64
-
65
55
  const moveSessionToProject = useCallback(
66
56
  async (sessionId: string, projectId: string) => {
67
57
  await moveToProjectMutation.mutateAsync({ sessionId, projectId });
@@ -71,7 +61,6 @@ export function useSessions() {
71
61
 
72
62
  return {
73
63
  sessions,
74
- groups,
75
64
  summarizingSessionId: summarizeMutation.isPending
76
65
  ? (summarizeMutation.variables as string)
77
66
  : null,
@@ -80,7 +69,6 @@ export function useSessions() {
80
69
  renameSession,
81
70
  forkSession,
82
71
  summarizeSession,
83
- moveSessionToGroup,
84
72
  moveSessionToProject,
85
73
  };
86
74
  }
@@ -3,6 +3,8 @@ import path from "path";
3
3
  import os from "os";
4
4
  import { WebSocket } from "ws";
5
5
  import { invalidateProject, invalidateAll } from "./jsonl-cache";
6
+ import { onStateFileChange, invalidateSessionName } from "../status-monitor";
7
+ import { STATES_DIR } from "../hooks/setup";
6
8
 
7
9
  const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects");
8
10
 
@@ -13,7 +15,7 @@ export function addUpdateClient(ws: WebSocket): void {
13
15
  ws.on("close", () => updateClients.delete(ws));
14
16
  }
15
17
 
16
- function broadcast(msg: object): void {
18
+ export function broadcast(msg: object): void {
17
19
  const data = JSON.stringify(msg);
18
20
  for (const ws of updateClients) {
19
21
  if (ws.readyState === WebSocket.OPEN) {
@@ -44,14 +46,21 @@ function handleFileChange(filePath: string): void {
44
46
 
45
47
  export function startWatcher(): void {
46
48
  try {
47
- const watcher = watch(CLAUDE_PROJECTS_DIR, {
49
+ // Watch Claude projects for session list updates
50
+ const projectsWatcher = watch(CLAUDE_PROJECTS_DIR, {
48
51
  ignoreInitial: true,
49
52
  depth: 2,
50
53
  ignored: [/node_modules/, /\.git/, /subagents/],
51
54
  });
52
55
 
53
- watcher.on("change", handleFileChange);
54
- watcher.on("add", (fp) => {
56
+ projectsWatcher.on("change", (fp) => {
57
+ handleFileChange(fp);
58
+ if (fp.endsWith(".jsonl")) {
59
+ const sessionId = path.basename(fp, ".jsonl");
60
+ invalidateSessionName(sessionId);
61
+ }
62
+ });
63
+ projectsWatcher.on("add", (fp) => {
55
64
  handleFileChange(fp);
56
65
  const relative = path.relative(CLAUDE_PROJECTS_DIR, fp);
57
66
  if (!relative.includes(path.sep)) {
@@ -59,12 +68,26 @@ export function startWatcher(): void {
59
68
  broadcast({ type: "projects-changed" });
60
69
  }
61
70
  });
62
- watcher.on("addDir", () => {
71
+ projectsWatcher.on("addDir", () => {
63
72
  invalidateAll();
64
73
  broadcast({ type: "projects-changed" });
65
74
  });
66
75
 
67
76
  console.log("> File watcher started on ~/.claude/projects/");
77
+
78
+ // Watch session state files written by hooks
79
+ const statesWatcher = watch(STATES_DIR, {
80
+ ignoreInitial: true,
81
+ depth: 0,
82
+ });
83
+
84
+ statesWatcher.on("change", onStateFileChange);
85
+ statesWatcher.on("add", onStateFileChange);
86
+ statesWatcher.on("unlink", onStateFileChange);
87
+
88
+ console.log(
89
+ "> State file watcher started on ~/.claude-deck/session-states/"
90
+ );
68
91
  } catch (err) {
69
92
  console.error("Failed to start file watcher:", err);
70
93
  }