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.
- package/README.md +16 -222
- package/package.json +18 -55
- package/.designs/beads-kanban-ui-bj0.md +0 -73
- package/.designs/beads-kanban-ui-qxq.md +0 -144
- package/.designs/epic-support.md +0 -282
- package/.env.local.example +0 -2
- package/.eslintrc.json +0 -3
- package/.gitattributes +0 -3
- package/.github/workflows/release.yml +0 -123
- package/.history/README_20260121193710.md +0 -227
- package/.history/README_20260121193918.md +0 -227
- package/.history/README_20260121193921.md +0 -227
- package/.history/README_20260121193933.md +0 -227
- package/.history/README_20260121193934.md +0 -227
- package/.history/README_20260121193944.md +0 -227
- package/.history/README_20260121193953.md +0 -227
- package/.history/src/app/page_20260121133429.tsx +0 -134
- package/.history/src/app/page_20260121133928.tsx +0 -134
- package/.history/src/app/page_20260121144850.tsx +0 -138
- package/.history/src/app/page_20260121144854.tsx +0 -138
- package/.history/src/app/page_20260121144858.tsx +0 -138
- package/.history/src/app/page_20260121144902.tsx +0 -138
- package/.history/src/app/page_20260121144906.tsx +0 -138
- package/.history/src/app/page_20260121144911.tsx +0 -138
- package/.history/src/app/page_20260121144928.tsx +0 -138
- package/.playwright-mcp/.playwright-mcp/morphing-dialog-wheel-scroll-fix.png +0 -0
- package/.playwright-mcp/beams-test.png +0 -0
- package/.playwright-mcp/card-verification.png +0 -0
- package/.playwright-mcp/design-doc-dialog-fix-verification.png +0 -0
- package/.playwright-mcp/dialog-width-test.png +0 -0
- package/.playwright-mcp/homepage.png +0 -0
- package/.playwright-mcp/morphing-dialog-expanded.png +0 -0
- package/.playwright-mcp/morphing-dialog-fixes-final.png +0 -0
- package/.playwright-mcp/morphing-dialog-open.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-08-31-529Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-09-23-431Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-10-28-773Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-10-47-432Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-11-12-350Z.png +0 -0
- package/.playwright-mcp/screenshot-after-click.png +0 -0
- package/.playwright-mcp/screenshot-after-dialog-click.png +0 -0
- package/.playwright-mcp/sheet-restored-after-dialog-close.png +0 -0
- package/.playwright-mcp/test-1-sheet-open-with-overlay.png +0 -0
- package/.playwright-mcp/test-2-morphing-dialog-with-overlay.png +0 -0
- package/.playwright-mcp/test-3-sheet-open-dark-overlay.png +0 -0
- package/.playwright-mcp/test-4-morphing-dialog-with-dark-overlay.png +0 -0
- package/.playwright-mcp/test-5-morphing-dialog-scrolled.png +0 -0
- package/.playwright-mcp/test-6-sheet-restored-after-dialog-close.png +0 -0
- package/.playwright-mcp/wheel-scroll-fixed.png +0 -0
- package/Screenshots/bead-detail.png +0 -0
- package/Screenshots/dashboard.png +0 -0
- package/Screenshots/kanban-board.png +0 -0
- package/components.json +0 -27
- package/logo/logo.svg +0 -1
- package/next.config.js +0 -9
- package/npm/README.md +0 -37
- package/npm/package.json +0 -20
- package/postcss.config.js +0 -6
- package/public/logo.svg +0 -1
- package/restart.sh +0 -5
- package/server/Cargo.lock +0 -1685
- package/server/Cargo.toml +0 -24
- package/server/src/db.rs +0 -570
- package/server/src/main.rs +0 -141
- package/server/src/routes/beads.rs +0 -413
- package/server/src/routes/cli.rs +0 -150
- package/server/src/routes/fs.rs +0 -360
- package/server/src/routes/git.rs +0 -169
- package/server/src/routes/mod.rs +0 -107
- package/server/src/routes/projects.rs +0 -177
- package/server/src/routes/watch.rs +0 -211
- package/src/app/globals.css +0 -101
- package/src/app/layout.tsx +0 -36
- package/src/app/page.tsx +0 -348
- package/src/app/project/kanban-board.tsx +0 -356
- package/src/app/project/page.tsx +0 -18
- package/src/app/settings/page.tsx +0 -224
- package/src/components/Beams.css +0 -5
- package/src/components/Beams.jsx +0 -307
- package/src/components/Galaxy.css +0 -5
- package/src/components/Galaxy.jsx +0 -333
- package/src/components/activity-timeline.tsx +0 -172
- package/src/components/add-project-dialog.tsx +0 -219
- package/src/components/bead-card.tsx +0 -196
- package/src/components/bead-detail.tsx +0 -306
- package/src/components/color-picker.tsx +0 -101
- package/src/components/comment-input.tsx +0 -155
- package/src/components/comment-list.tsx +0 -147
- package/src/components/dependency-badge.tsx +0 -106
- package/src/components/design-doc-dialog.tsx +0 -58
- package/src/components/design-doc-preview.tsx +0 -97
- package/src/components/design-doc-viewer.tsx +0 -199
- package/src/components/editable-project-name.tsx +0 -178
- package/src/components/epic-card.tsx +0 -263
- package/src/components/folder-browser.tsx +0 -273
- package/src/components/footer.tsx +0 -27
- package/src/components/kanban/default.tsx +0 -184
- package/src/components/kanban-column.tsx +0 -167
- package/src/components/project-card.tsx +0 -191
- package/src/components/quick-filter-bar.tsx +0 -279
- package/src/components/scan-directory-dialog.tsx +0 -368
- package/src/components/status-donut.tsx +0 -197
- package/src/components/subtask-list.tsx +0 -128
- package/src/components/tag-picker.tsx +0 -252
- package/src/components/ui/.gitkeep +0 -0
- package/src/components/ui/alert-dialog.tsx +0 -141
- package/src/components/ui/avatar.tsx +0 -67
- package/src/components/ui/badge.tsx +0 -230
- package/src/components/ui/button.tsx +0 -433
- package/src/components/ui/card/index.tsx +0 -24
- package/src/components/ui/card/roiui-card.module.css +0 -197
- package/src/components/ui/card/roiui-card.tsx +0 -154
- package/src/components/ui/card/shadcn-card.tsx +0 -76
- package/src/components/ui/chart.tsx +0 -369
- package/src/components/ui/dialog.tsx +0 -122
- package/src/components/ui/dropdown-menu.tsx +0 -201
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/kanban.tsx +0 -522
- package/src/components/ui/morphing-dialog.tsx +0 -457
- package/src/components/ui/popover.tsx +0 -33
- package/src/components/ui/progress.tsx +0 -28
- package/src/components/ui/scroll-area.tsx +0 -48
- package/src/components/ui/select.tsx +0 -159
- package/src/components/ui/separator.tsx +0 -31
- package/src/components/ui/sheet.tsx +0 -142
- package/src/components/ui/skeleton.tsx +0 -15
- package/src/components/ui/toast.tsx +0 -129
- package/src/components/ui/toaster.tsx +0 -35
- package/src/components/ui/tooltip.tsx +0 -30
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-bead-filters.ts +0 -261
- package/src/hooks/use-beads.ts +0 -162
- package/src/hooks/use-branch-statuses.ts +0 -161
- package/src/hooks/use-epics.ts +0 -173
- package/src/hooks/use-file-watcher.ts +0 -111
- package/src/hooks/use-keyboard-navigation.ts +0 -282
- package/src/hooks/use-project.ts +0 -61
- package/src/hooks/use-projects.ts +0 -93
- package/src/hooks/use-toast.ts +0 -194
- package/src/hooks/useClickOutside.tsx +0 -26
- package/src/lib/.gitkeep +0 -0
- package/src/lib/api.ts +0 -186
- package/src/lib/beads-parser.ts +0 -252
- package/src/lib/cli.ts +0 -193
- package/src/lib/db.ts +0 -145
- package/src/lib/design-doc.ts +0 -74
- package/src/lib/epic-parser.ts +0 -242
- package/src/lib/git.ts +0 -102
- package/src/lib/utils.ts +0 -12
- package/src/types/index.ts +0 -107
- package/tailwind.config.ts +0 -85
- package/tsconfig.json +0 -26
- /package/{npm/bin → bin}/cli.js +0 -0
- /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
|
-
}
|
package/src/hooks/use-beads.ts
DELETED
|
@@ -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
|
-
}
|