beads-kanban-ui 0.1.0 → 0.1.1

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 (154) hide show
  1. package/README.md +16 -222
  2. package/package.json +18 -55
  3. package/.designs/beads-kanban-ui-bj0.md +0 -73
  4. package/.designs/beads-kanban-ui-qxq.md +0 -144
  5. package/.designs/epic-support.md +0 -282
  6. package/.env.local.example +0 -2
  7. package/.eslintrc.json +0 -3
  8. package/.gitattributes +0 -3
  9. package/.github/workflows/release.yml +0 -123
  10. package/.history/README_20260121193710.md +0 -227
  11. package/.history/README_20260121193918.md +0 -227
  12. package/.history/README_20260121193921.md +0 -227
  13. package/.history/README_20260121193933.md +0 -227
  14. package/.history/README_20260121193934.md +0 -227
  15. package/.history/README_20260121193944.md +0 -227
  16. package/.history/README_20260121193953.md +0 -227
  17. package/.history/src/app/page_20260121133429.tsx +0 -134
  18. package/.history/src/app/page_20260121133928.tsx +0 -134
  19. package/.history/src/app/page_20260121144850.tsx +0 -138
  20. package/.history/src/app/page_20260121144854.tsx +0 -138
  21. package/.history/src/app/page_20260121144858.tsx +0 -138
  22. package/.history/src/app/page_20260121144902.tsx +0 -138
  23. package/.history/src/app/page_20260121144906.tsx +0 -138
  24. package/.history/src/app/page_20260121144911.tsx +0 -138
  25. package/.history/src/app/page_20260121144928.tsx +0 -138
  26. package/.playwright-mcp/.playwright-mcp/morphing-dialog-wheel-scroll-fix.png +0 -0
  27. package/.playwright-mcp/beams-test.png +0 -0
  28. package/.playwright-mcp/card-verification.png +0 -0
  29. package/.playwright-mcp/design-doc-dialog-fix-verification.png +0 -0
  30. package/.playwright-mcp/dialog-width-test.png +0 -0
  31. package/.playwright-mcp/homepage.png +0 -0
  32. package/.playwright-mcp/morphing-dialog-expanded.png +0 -0
  33. package/.playwright-mcp/morphing-dialog-fixes-final.png +0 -0
  34. package/.playwright-mcp/morphing-dialog-open.png +0 -0
  35. package/.playwright-mcp/page-2026-01-21T14-08-31-529Z.png +0 -0
  36. package/.playwright-mcp/page-2026-01-21T14-09-23-431Z.png +0 -0
  37. package/.playwright-mcp/page-2026-01-21T14-10-28-773Z.png +0 -0
  38. package/.playwright-mcp/page-2026-01-21T14-10-47-432Z.png +0 -0
  39. package/.playwright-mcp/page-2026-01-21T14-11-12-350Z.png +0 -0
  40. package/.playwright-mcp/screenshot-after-click.png +0 -0
  41. package/.playwright-mcp/screenshot-after-dialog-click.png +0 -0
  42. package/.playwright-mcp/sheet-restored-after-dialog-close.png +0 -0
  43. package/.playwright-mcp/test-1-sheet-open-with-overlay.png +0 -0
  44. package/.playwright-mcp/test-2-morphing-dialog-with-overlay.png +0 -0
  45. package/.playwright-mcp/test-3-sheet-open-dark-overlay.png +0 -0
  46. package/.playwright-mcp/test-4-morphing-dialog-with-dark-overlay.png +0 -0
  47. package/.playwright-mcp/test-5-morphing-dialog-scrolled.png +0 -0
  48. package/.playwright-mcp/test-6-sheet-restored-after-dialog-close.png +0 -0
  49. package/.playwright-mcp/wheel-scroll-fixed.png +0 -0
  50. package/Screenshots/bead-detail.png +0 -0
  51. package/Screenshots/dashboard.png +0 -0
  52. package/Screenshots/kanban-board.png +0 -0
  53. package/components.json +0 -27
  54. package/logo/logo.svg +0 -1
  55. package/next.config.js +0 -9
  56. package/npm/README.md +0 -37
  57. package/npm/package.json +0 -20
  58. package/postcss.config.js +0 -6
  59. package/public/logo.svg +0 -1
  60. package/restart.sh +0 -5
  61. package/server/Cargo.lock +0 -1685
  62. package/server/Cargo.toml +0 -24
  63. package/server/src/db.rs +0 -570
  64. package/server/src/main.rs +0 -141
  65. package/server/src/routes/beads.rs +0 -413
  66. package/server/src/routes/cli.rs +0 -150
  67. package/server/src/routes/fs.rs +0 -360
  68. package/server/src/routes/git.rs +0 -169
  69. package/server/src/routes/mod.rs +0 -107
  70. package/server/src/routes/projects.rs +0 -177
  71. package/server/src/routes/watch.rs +0 -211
  72. package/src/app/globals.css +0 -101
  73. package/src/app/layout.tsx +0 -36
  74. package/src/app/page.tsx +0 -348
  75. package/src/app/project/kanban-board.tsx +0 -356
  76. package/src/app/project/page.tsx +0 -18
  77. package/src/app/settings/page.tsx +0 -224
  78. package/src/components/Beams.css +0 -5
  79. package/src/components/Beams.jsx +0 -307
  80. package/src/components/Galaxy.css +0 -5
  81. package/src/components/Galaxy.jsx +0 -333
  82. package/src/components/activity-timeline.tsx +0 -172
  83. package/src/components/add-project-dialog.tsx +0 -219
  84. package/src/components/bead-card.tsx +0 -196
  85. package/src/components/bead-detail.tsx +0 -306
  86. package/src/components/color-picker.tsx +0 -101
  87. package/src/components/comment-input.tsx +0 -155
  88. package/src/components/comment-list.tsx +0 -147
  89. package/src/components/dependency-badge.tsx +0 -106
  90. package/src/components/design-doc-dialog.tsx +0 -58
  91. package/src/components/design-doc-preview.tsx +0 -97
  92. package/src/components/design-doc-viewer.tsx +0 -199
  93. package/src/components/editable-project-name.tsx +0 -178
  94. package/src/components/epic-card.tsx +0 -263
  95. package/src/components/folder-browser.tsx +0 -273
  96. package/src/components/footer.tsx +0 -27
  97. package/src/components/kanban/default.tsx +0 -184
  98. package/src/components/kanban-column.tsx +0 -167
  99. package/src/components/project-card.tsx +0 -191
  100. package/src/components/quick-filter-bar.tsx +0 -279
  101. package/src/components/scan-directory-dialog.tsx +0 -368
  102. package/src/components/status-donut.tsx +0 -197
  103. package/src/components/subtask-list.tsx +0 -128
  104. package/src/components/tag-picker.tsx +0 -252
  105. package/src/components/ui/.gitkeep +0 -0
  106. package/src/components/ui/alert-dialog.tsx +0 -141
  107. package/src/components/ui/avatar.tsx +0 -67
  108. package/src/components/ui/badge.tsx +0 -230
  109. package/src/components/ui/button.tsx +0 -433
  110. package/src/components/ui/card/index.tsx +0 -24
  111. package/src/components/ui/card/roiui-card.module.css +0 -197
  112. package/src/components/ui/card/roiui-card.tsx +0 -154
  113. package/src/components/ui/card/shadcn-card.tsx +0 -76
  114. package/src/components/ui/chart.tsx +0 -369
  115. package/src/components/ui/dialog.tsx +0 -122
  116. package/src/components/ui/dropdown-menu.tsx +0 -201
  117. package/src/components/ui/input.tsx +0 -22
  118. package/src/components/ui/kanban.tsx +0 -522
  119. package/src/components/ui/morphing-dialog.tsx +0 -457
  120. package/src/components/ui/popover.tsx +0 -33
  121. package/src/components/ui/progress.tsx +0 -28
  122. package/src/components/ui/scroll-area.tsx +0 -48
  123. package/src/components/ui/select.tsx +0 -159
  124. package/src/components/ui/separator.tsx +0 -31
  125. package/src/components/ui/sheet.tsx +0 -142
  126. package/src/components/ui/skeleton.tsx +0 -15
  127. package/src/components/ui/toast.tsx +0 -129
  128. package/src/components/ui/toaster.tsx +0 -35
  129. package/src/components/ui/tooltip.tsx +0 -30
  130. package/src/hooks/.gitkeep +0 -0
  131. package/src/hooks/use-bead-filters.ts +0 -261
  132. package/src/hooks/use-beads.ts +0 -162
  133. package/src/hooks/use-branch-statuses.ts +0 -161
  134. package/src/hooks/use-epics.ts +0 -173
  135. package/src/hooks/use-file-watcher.ts +0 -111
  136. package/src/hooks/use-keyboard-navigation.ts +0 -282
  137. package/src/hooks/use-project.ts +0 -61
  138. package/src/hooks/use-projects.ts +0 -93
  139. package/src/hooks/use-toast.ts +0 -194
  140. package/src/hooks/useClickOutside.tsx +0 -26
  141. package/src/lib/.gitkeep +0 -0
  142. package/src/lib/api.ts +0 -186
  143. package/src/lib/beads-parser.ts +0 -252
  144. package/src/lib/cli.ts +0 -193
  145. package/src/lib/db.ts +0 -145
  146. package/src/lib/design-doc.ts +0 -74
  147. package/src/lib/epic-parser.ts +0 -242
  148. package/src/lib/git.ts +0 -102
  149. package/src/lib/utils.ts +0 -12
  150. package/src/types/index.ts +0 -107
  151. package/tailwind.config.ts +0 -85
  152. package/tsconfig.json +0 -26
  153. /package/{npm/bin → bin}/cli.js +0 -0
  154. /package/{npm/scripts → scripts}/postinstall.js +0 -0
@@ -1,261 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * Hook for filtering beads with debounced search and multi-criteria filtering.
5
- *
6
- * Provides search (with 300ms debounce), status, priority, and owner filtering
7
- * with a clean API for the kanban board.
8
- */
9
-
10
- import { useState, useMemo, useCallback, useEffect } from "react";
11
- import type { Bead, BeadStatus } from "@/types";
12
-
13
- /**
14
- * Sort field options
15
- */
16
- export type SortField = "ticket_number" | "created_at";
17
-
18
- /**
19
- * Sort direction options
20
- */
21
- export type SortDirection = "asc" | "desc";
22
-
23
- /**
24
- * Filter state for beads
25
- */
26
- export interface BeadFilters {
27
- /** Search query for title and description (case-insensitive) */
28
- search: string;
29
- /** Status filter - empty array means all statuses */
30
- statuses: BeadStatus[];
31
- /** Priority filter - empty array means all priorities (0-4) */
32
- priorities: number[];
33
- /** Owner/agent filter - empty array means all owners */
34
- owners: string[];
35
- /** Sort field */
36
- sortField: SortField;
37
- /** Sort direction */
38
- sortDirection: SortDirection;
39
- /** Filter to items updated (worked on) today */
40
- todayOnly: boolean;
41
- }
42
-
43
- /**
44
- * Result type for the useBeadFilters hook
45
- */
46
- export interface UseBeadFiltersResult {
47
- /** Current filter state */
48
- filters: BeadFilters;
49
- /** Update filters (partial update supported) */
50
- setFilters: (filters: Partial<BeadFilters>) => void;
51
- /** Beads after applying all filters */
52
- filteredBeads: Bead[];
53
- /** Reset all filters to default */
54
- clearFilters: () => void;
55
- /** Whether any filters are active */
56
- hasActiveFilters: boolean;
57
- /** Count of active filter categories */
58
- activeFilterCount: number;
59
- /** Unique owners extracted from beads */
60
- availableOwners: string[];
61
- /** Debounced search value (for display) */
62
- debouncedSearch: string;
63
- }
64
-
65
- /**
66
- * Default/empty filter state
67
- */
68
- const DEFAULT_FILTERS: BeadFilters = {
69
- search: "",
70
- statuses: [],
71
- priorities: [],
72
- owners: [],
73
- sortField: "created_at",
74
- sortDirection: "desc",
75
- todayOnly: false,
76
- };
77
-
78
- /**
79
- * Hook to filter beads with debounced search and multi-criteria filtering.
80
- *
81
- * @param beads - Array of beads to filter
82
- * @param debounceMs - Debounce delay for search input (default 300ms)
83
- * @returns Filter state, setters, and filtered beads
84
- *
85
- * @example
86
- * ```tsx
87
- * function KanbanBoard({ beads }: { beads: Bead[] }) {
88
- * const {
89
- * filters,
90
- * setFilters,
91
- * filteredBeads,
92
- * clearFilters,
93
- * hasActiveFilters,
94
- * activeFilterCount,
95
- * } = useBeadFilters(beads);
96
- *
97
- * return (
98
- * <>
99
- * <input
100
- * value={filters.search}
101
- * onChange={(e) => setFilters({ search: e.target.value })}
102
- * />
103
- * {hasActiveFilters && (
104
- * <button onClick={clearFilters}>
105
- * Clear ({activeFilterCount})
106
- * </button>
107
- * )}
108
- * <BeadList beads={filteredBeads} />
109
- * </>
110
- * );
111
- * }
112
- * ```
113
- */
114
- export function useBeadFilters(
115
- beads: Bead[],
116
- ticketNumbers: Map<string, number>,
117
- debounceMs: number = 300
118
- ): UseBeadFiltersResult {
119
- // Filter state
120
- const [filters, setFiltersState] = useState<BeadFilters>(DEFAULT_FILTERS);
121
-
122
- // Debounced search value
123
- const [debouncedSearch, setDebouncedSearch] = useState("");
124
-
125
- // Debounce the search input
126
- useEffect(() => {
127
- const timer = setTimeout(() => {
128
- setDebouncedSearch(filters.search);
129
- }, debounceMs);
130
-
131
- return () => clearTimeout(timer);
132
- }, [filters.search, debounceMs]);
133
-
134
- /**
135
- * Update filters with partial state
136
- */
137
- const setFilters = useCallback((partialFilters: Partial<BeadFilters>) => {
138
- setFiltersState((prev) => ({
139
- ...prev,
140
- ...partialFilters,
141
- }));
142
- }, []);
143
-
144
- /**
145
- * Reset all filters to defaults
146
- */
147
- const clearFilters = useCallback(() => {
148
- setFiltersState(DEFAULT_FILTERS);
149
- setDebouncedSearch("");
150
- }, []);
151
-
152
- /**
153
- * Extract unique owners from all beads
154
- */
155
- const availableOwners = useMemo(() => {
156
- const owners = new Set<string>();
157
- beads.forEach((bead) => {
158
- if (bead.owner) {
159
- owners.add(bead.owner);
160
- }
161
- });
162
- return Array.from(owners).sort();
163
- }, [beads]);
164
-
165
- /**
166
- * Apply all filters to beads and sort
167
- */
168
- const filteredBeads = useMemo(() => {
169
- const { sortField, sortDirection } = filters;
170
-
171
- // Filter beads
172
- const filtered = beads.filter((bead) => {
173
- // Search filter (uses debounced value for performance)
174
- if (debouncedSearch) {
175
- const searchLower = debouncedSearch.toLowerCase();
176
- const matchesSearch =
177
- bead.title.toLowerCase().includes(searchLower) ||
178
- (bead.description &&
179
- bead.description.toLowerCase().includes(searchLower));
180
- if (!matchesSearch) return false;
181
- }
182
-
183
- // Status filter
184
- if (filters.statuses.length > 0) {
185
- if (!filters.statuses.includes(bead.status)) return false;
186
- }
187
-
188
- // Priority filter
189
- if (filters.priorities.length > 0) {
190
- if (!filters.priorities.includes(bead.priority)) return false;
191
- }
192
-
193
- // Owner filter
194
- if (filters.owners.length > 0) {
195
- if (!filters.owners.includes(bead.owner)) return false;
196
- }
197
-
198
- // Today filter - items updated (worked on) today, regardless of status
199
- if (filters.todayOnly) {
200
- const today = new Date().toISOString().split("T")[0];
201
- const updatedToday = bead.updated_at.startsWith(today);
202
- if (!updatedToday) return false;
203
- }
204
-
205
- return true;
206
- });
207
-
208
- // Sort the filtered results (use toSorted for immutability)
209
- const sorted = filtered.toSorted((a, b) => {
210
- if (sortField === "ticket_number") {
211
- const aNum = ticketNumbers.get(a.id) ?? 0;
212
- const bNum = ticketNumbers.get(b.id) ?? 0;
213
- return sortDirection === "asc" ? aNum - bNum : bNum - aNum;
214
- }
215
- // created_at sort
216
- const aDate = new Date(a.created_at).getTime();
217
- const bDate = new Date(b.created_at).getTime();
218
- return sortDirection === "asc" ? aDate - bDate : bDate - aDate;
219
- });
220
-
221
- return sorted;
222
- }, [beads, debouncedSearch, filters, ticketNumbers]);
223
-
224
- /**
225
- * Check if any filters are active
226
- */
227
- const hasActiveFilters = useMemo(() => {
228
- return (
229
- filters.search !== "" ||
230
- filters.statuses.length > 0 ||
231
- filters.priorities.length > 0 ||
232
- filters.owners.length > 0 ||
233
- filters.todayOnly ||
234
- filters.sortField !== DEFAULT_FILTERS.sortField ||
235
- filters.sortDirection !== DEFAULT_FILTERS.sortDirection
236
- );
237
- }, [filters]);
238
-
239
- /**
240
- * Count active filter categories (for badge)
241
- */
242
- const activeFilterCount = useMemo(() => {
243
- let count = 0;
244
- if (filters.statuses.length > 0) count++;
245
- if (filters.priorities.length > 0) count++;
246
- if (filters.owners.length > 0) count++;
247
- if (filters.todayOnly) count++;
248
- return count;
249
- }, [filters]);
250
-
251
- return {
252
- filters,
253
- setFilters,
254
- filteredBeads,
255
- clearFilters,
256
- hasActiveFilters,
257
- activeFilterCount,
258
- availableOwners,
259
- debouncedSearch,
260
- };
261
- }
@@ -1,162 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * Hook for loading and managing beads with real-time file watching.
5
- *
6
- * Combines the beads parser with file watcher to provide automatic
7
- * updates when the issues.jsonl file changes.
8
- */
9
-
10
- import { useState, useEffect, useCallback, useRef } from "react";
11
- import type { Bead, BeadStatus } from "@/types";
12
- import {
13
- loadProjectBeads,
14
- groupBeadsByStatus,
15
- assignTicketNumbers,
16
- } from "@/lib/beads-parser";
17
- import { useFileWatcher } from "@/hooks/use-file-watcher";
18
-
19
- /**
20
- * Result type for the useBeads hook
21
- */
22
- export interface UseBeadsResult {
23
- /** Array of all beads from the project */
24
- beads: Bead[];
25
- /** Beads grouped by status for kanban columns */
26
- beadsByStatus: Record<BeadStatus, Bead[]>;
27
- /** Map of bead ID to sequential ticket number (1-indexed by creation order) */
28
- ticketNumbers: Map<string, number>;
29
- /** Whether beads are currently being loaded */
30
- isLoading: boolean;
31
- /** Any error that occurred during loading */
32
- error: Error | null;
33
- /** Manually refresh beads from the file */
34
- refresh: () => Promise<void>;
35
- }
36
-
37
- /**
38
- * Empty grouped beads object for initial state
39
- */
40
- const EMPTY_GROUPED: Record<BeadStatus, Bead[]> = {
41
- open: [],
42
- in_progress: [],
43
- inreview: [],
44
- closed: [],
45
- };
46
-
47
- /**
48
- * Hook to load and watch beads from a project directory.
49
- *
50
- * Automatically refreshes when the issues.jsonl file changes.
51
- *
52
- * @param projectPath - The absolute path to the project root
53
- * @returns Object containing beads, grouped beads, loading state, error, and refresh function
54
- *
55
- * @example
56
- * ```tsx
57
- * function KanbanBoard({ projectPath }: { projectPath: string }) {
58
- * const { beadsByStatus, isLoading, error, refresh } = useBeads(projectPath);
59
- *
60
- * if (isLoading) return <Loading />;
61
- * if (error) return <Error message={error.message} />;
62
- *
63
- * return (
64
- * <div>
65
- * <Column title="Open" beads={beadsByStatus.open} />
66
- * <Column title="In Progress" beads={beadsByStatus.in_progress} />
67
- * <Column title="In Review" beads={beadsByStatus.inreview} />
68
- * <Column title="Closed" beads={beadsByStatus.closed} />
69
- * </div>
70
- * );
71
- * }
72
- * ```
73
- */
74
- export function useBeads(projectPath: string): UseBeadsResult {
75
- const [beads, setBeads] = useState<Bead[]>([]);
76
- const [beadsByStatus, setBeadsByStatus] =
77
- useState<Record<BeadStatus, Bead[]>>(EMPTY_GROUPED);
78
- const [ticketNumbers, setTicketNumbers] = useState<Map<string, number>>(
79
- new Map()
80
- );
81
- const [isLoading, setIsLoading] = useState(true);
82
- const [error, setError] = useState<Error | null>(null);
83
-
84
- // Track if initial load has completed
85
- const hasLoadedRef = useRef(false);
86
-
87
- /**
88
- * Load beads from the project directory
89
- */
90
- const loadBeads = useCallback(async () => {
91
- if (!projectPath) {
92
- setBeads([]);
93
- setBeadsByStatus(EMPTY_GROUPED);
94
- setTicketNumbers(new Map());
95
- setIsLoading(false);
96
- return;
97
- }
98
-
99
- // Only show loading on initial load, not on refreshes
100
- if (!hasLoadedRef.current) {
101
- setIsLoading(true);
102
- }
103
-
104
- try {
105
- const loadedBeads = await loadProjectBeads(projectPath);
106
- const grouped = groupBeadsByStatus(loadedBeads);
107
- const tickets = assignTicketNumbers(loadedBeads);
108
-
109
- setBeads(loadedBeads);
110
- setBeadsByStatus(grouped);
111
- setTicketNumbers(tickets);
112
- setError(null);
113
- hasLoadedRef.current = true;
114
- } catch (err) {
115
- const error = err instanceof Error ? err : new Error(String(err));
116
- setError(error);
117
- console.error("Failed to load beads:", error);
118
- } finally {
119
- setIsLoading(false);
120
- }
121
- }, [projectPath]);
122
-
123
- /**
124
- * Public refresh function for manual reload
125
- */
126
- const refresh = useCallback(async () => {
127
- await loadBeads();
128
- }, [loadBeads]);
129
-
130
- // Initial load when project path changes
131
- useEffect(() => {
132
- hasLoadedRef.current = false;
133
- loadBeads();
134
- }, [loadBeads]);
135
-
136
- // Set up file watcher for real-time updates
137
- // Note: useFileWatcher expects the project root path, not the full issues.jsonl path,
138
- // because the backend watch API appends .beads/issues.jsonl to the provided path
139
- const { error: watchError } = useFileWatcher(
140
- projectPath,
141
- loadBeads,
142
- 100 // 100ms debounce as per spec
143
- );
144
-
145
- // Combine any watch error with load error
146
- useEffect(() => {
147
- if (watchError && !error) {
148
- // Only log watch errors, don't surface them as main error
149
- // since the app can still function without file watching
150
- console.warn("File watcher error:", watchError);
151
- }
152
- }, [watchError, error]);
153
-
154
- return {
155
- beads,
156
- beadsByStatus,
157
- ticketNumbers,
158
- isLoading,
159
- error,
160
- refresh,
161
- };
162
- }
@@ -1,161 +0,0 @@
1
- /**
2
- * Hook for fetching branch statuses for multiple beads
3
- *
4
- * Efficiently fetches branch status (exists, ahead, behind) for all beads
5
- * in a project and keeps the data updated.
6
- */
7
-
8
- import { useState, useEffect, useCallback, useRef } from "react";
9
- import { getBatchBranchStatus, type BranchStatus } from "@/lib/git";
10
-
11
- /**
12
- * Result type for the useBranchStatuses hook
13
- */
14
- export interface UseBranchStatusesResult {
15
- /** Map of bead ID to branch status */
16
- statuses: Record<string, BranchStatus>;
17
- /** Whether statuses are currently being loaded */
18
- isLoading: boolean;
19
- /** Any error that occurred during loading */
20
- error: Error | null;
21
- /** Manually refresh branch statuses */
22
- refresh: () => Promise<void>;
23
- }
24
-
25
- /**
26
- * Convert bead ID to expected branch name format
27
- *
28
- * @param beadId - The bead ID (e.g., "BD-001" or "project-abc123")
29
- * @returns The expected branch name (e.g., "bd-BD-001")
30
- */
31
- function beadIdToBranchName(beadId: string): string {
32
- // If already has bd- prefix, use as-is (lowercase)
33
- if (beadId.toLowerCase().startsWith("bd-")) {
34
- return `bd-${beadId}`;
35
- }
36
- // Otherwise prefix with bd-
37
- return `bd-${beadId}`;
38
- }
39
-
40
- /**
41
- * Hook to fetch and track branch statuses for beads
42
- *
43
- * @param projectPath - Absolute path to the project git repository
44
- * @param beadIds - Array of bead IDs to check branch status for
45
- * @returns Object containing statuses map, loading state, error, and refresh function
46
- *
47
- * @example
48
- * ```tsx
49
- * function KanbanBoard({ projectPath, beads }) {
50
- * const beadIds = beads.map(b => b.id);
51
- * const { statuses, isLoading } = useBranchStatuses(projectPath, beadIds);
52
- *
53
- * return beads.map(bead => (
54
- * <BeadCard
55
- * key={bead.id}
56
- * bead={bead}
57
- * branchStatus={statuses[bead.id]}
58
- * />
59
- * ));
60
- * }
61
- * ```
62
- */
63
- export function useBranchStatuses(
64
- projectPath: string,
65
- beadIds: string[]
66
- ): UseBranchStatusesResult {
67
- const [statuses, setStatuses] = useState<Record<string, BranchStatus>>({});
68
- const [isLoading, setIsLoading] = useState(true);
69
- const [error, setError] = useState<Error | null>(null);
70
-
71
- // Track if initial load has completed
72
- const hasLoadedRef = useRef(false);
73
-
74
- // Store previous bead IDs to detect changes
75
- const prevBeadIdsRef = useRef<string[]>([]);
76
-
77
- /**
78
- * Load branch statuses for all beads
79
- */
80
- const loadStatuses = useCallback(async () => {
81
- if (!projectPath || beadIds.length === 0) {
82
- setStatuses({});
83
- setIsLoading(false);
84
- return;
85
- }
86
-
87
- // Only show loading on initial load
88
- if (!hasLoadedRef.current) {
89
- setIsLoading(true);
90
- }
91
-
92
- try {
93
- // Convert bead IDs to branch names
94
- const branchNames = beadIds.map(beadIdToBranchName);
95
-
96
- // Fetch all branch statuses in batch
97
- const branchStatuses = await getBatchBranchStatus(projectPath, branchNames);
98
-
99
- // Map back to bead IDs
100
- const beadStatuses: Record<string, BranchStatus> = {};
101
- beadIds.forEach((beadId) => {
102
- const branchName = beadIdToBranchName(beadId);
103
- beadStatuses[beadId] = branchStatuses[branchName] || {
104
- exists: false,
105
- ahead: 0,
106
- behind: 0,
107
- };
108
- });
109
-
110
- setStatuses(beadStatuses);
111
- setError(null);
112
- hasLoadedRef.current = true;
113
- } catch (err) {
114
- const error = err instanceof Error ? err : new Error(String(err));
115
- setError(error);
116
- console.error("Failed to load branch statuses:", error);
117
- } finally {
118
- setIsLoading(false);
119
- }
120
- }, [projectPath, beadIds]);
121
-
122
- /**
123
- * Public refresh function for manual reload
124
- */
125
- const refresh = useCallback(async () => {
126
- await loadStatuses();
127
- }, [loadStatuses]);
128
-
129
- // Load statuses when project path or bead IDs change
130
- useEffect(() => {
131
- // Check if bead IDs have actually changed
132
- const beadIdsChanged =
133
- beadIds.length !== prevBeadIdsRef.current.length ||
134
- beadIds.some((id, i) => id !== prevBeadIdsRef.current[i]);
135
-
136
- if (beadIdsChanged) {
137
- prevBeadIdsRef.current = [...beadIds];
138
- hasLoadedRef.current = false;
139
- }
140
-
141
- loadStatuses();
142
- }, [loadStatuses, beadIds]);
143
-
144
- // Set up periodic refresh (every 30 seconds)
145
- useEffect(() => {
146
- if (!projectPath || beadIds.length === 0) return;
147
-
148
- const intervalId = setInterval(() => {
149
- loadStatuses();
150
- }, 30000);
151
-
152
- return () => clearInterval(intervalId);
153
- }, [projectPath, beadIds.length, loadStatuses]);
154
-
155
- return {
156
- statuses,
157
- isLoading,
158
- error,
159
- refresh,
160
- };
161
- }