jettypod 4.4.120 → 4.4.121
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/.env +2 -1
- package/Cargo.lock +6450 -0
- package/Cargo.toml +35 -0
- package/README.md +5 -1
- package/TAURI-MIGRATION-PLAN.md +840 -0
- package/apps/dashboard/app/connect-claude/page.tsx +5 -6
- package/apps/dashboard/app/decision/[id]/page.tsx +54 -49
- package/apps/dashboard/app/demo/gates/page.tsx +3 -5
- package/apps/dashboard/app/design-system/page.tsx +1 -1
- package/apps/dashboard/app/globals.css +74 -2
- package/apps/dashboard/app/install-claude/page.tsx +3 -5
- package/apps/dashboard/app/login/page.tsx +17 -20
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +60 -12
- package/apps/dashboard/app/signup/page.tsx +14 -17
- package/apps/dashboard/app/subscribe/page.tsx +0 -2
- package/apps/dashboard/app/tests/page.tsx +37 -4
- package/apps/dashboard/app/welcome/page.tsx +12 -15
- package/apps/dashboard/app/work/[id]/page.tsx +90 -75
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +70 -61
- package/apps/dashboard/components/CardMenu.tsx +0 -1
- package/apps/dashboard/components/ClaudePanel.tsx +541 -283
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
- package/apps/dashboard/components/CopyableId.tsx +1 -2
- package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
- package/apps/dashboard/components/DragContext.tsx +132 -62
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +5 -6
- package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +0 -1
- package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
- package/apps/dashboard/components/EpicGroup.tsx +100 -70
- package/apps/dashboard/components/GateCard.tsx +0 -1
- package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
- package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
- package/apps/dashboard/components/JettyLoader.tsx +0 -1
- package/apps/dashboard/components/KanbanBoard.tsx +319 -173
- package/apps/dashboard/components/KanbanCard.tsx +341 -107
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
- package/apps/dashboard/components/MainNav.tsx +24 -25
- package/apps/dashboard/components/MessageBlock.tsx +93 -16
- package/apps/dashboard/components/ModeStartCard.tsx +0 -1
- package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
- package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
- package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
- package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
- package/apps/dashboard/components/ReviewFooter.tsx +12 -14
- package/apps/dashboard/components/SessionList.tsx +0 -1
- package/apps/dashboard/components/SubscribeContent.tsx +40 -11
- package/apps/dashboard/components/TestTree.tsx +1 -2
- package/apps/dashboard/components/TipCard.tsx +2 -4
- package/apps/dashboard/components/Toast.tsx +0 -1
- package/apps/dashboard/components/TypeIcon.tsx +7 -8
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
- package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
- package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
- package/apps/dashboard/components/WorkItemTree.tsx +2 -4
- package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
- package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
- package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +20 -73
- package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
- package/apps/dashboard/components/ui/Button.tsx +1 -1
- package/apps/dashboard/components/ui/Input.tsx +1 -1
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
- package/apps/dashboard/contexts/UsageContext.tsx +62 -31
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1302
- package/apps/dashboard/lib/environment-config.ts +173 -0
- package/apps/dashboard/lib/environment-verification.ts +119 -0
- package/apps/dashboard/lib/kanban-utils.ts +226 -26
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/service-recovery.ts +326 -0
- package/apps/dashboard/lib/session-state-machine.ts +1 -0
- package/apps/dashboard/lib/session-state-utils.ts +0 -164
- package/apps/dashboard/lib/session-stream-manager.ts +253 -122
- package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
- package/apps/dashboard/lib/tauri-bridge.ts +102 -0
- package/apps/dashboard/lib/tauri.ts +106 -0
- package/apps/dashboard/lib/utils.ts +3 -3
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -33
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/pier-icon.png +0 -0
- package/apps/dashboard/public/star-icon.png +0 -0
- package/apps/dashboard/public/wrench-icon.png +0 -0
- package/apps/dashboard/scripts/tauri-build.js +228 -0
- package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
- package/apps/dashboard/src/main.tsx +12 -0
- package/apps/dashboard/src/router.tsx +107 -0
- package/apps/dashboard/src/vite-env.d.ts +1 -0
- package/apps/dashboard/tsconfig.json +7 -12
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
- package/apps/dashboard/vite.config.ts +33 -0
- package/apps/update-server/src/index.ts +167 -30
- package/claude-hooks/global-guardrails.js +14 -13
- package/crates/jettypod-cli/Cargo.toml +19 -0
- package/crates/jettypod-cli/src/commands.rs +1249 -0
- package/crates/jettypod-cli/src/main.rs +595 -0
- package/crates/jettypod-core/Cargo.toml +26 -0
- package/crates/jettypod-core/build.rs +98 -0
- package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
- package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
- package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
- package/crates/jettypod-core/src/auth.rs +294 -0
- package/crates/jettypod-core/src/config.rs +397 -0
- package/crates/jettypod-core/src/db/mod.rs +507 -0
- package/crates/jettypod-core/src/db/recovery.rs +114 -0
- package/crates/jettypod-core/src/db/startup.rs +101 -0
- package/crates/jettypod-core/src/db/validate.rs +149 -0
- package/crates/jettypod-core/src/error.rs +76 -0
- package/crates/jettypod-core/src/git.rs +458 -0
- package/crates/jettypod-core/src/lib.rs +20 -0
- package/crates/jettypod-core/src/sessions.rs +625 -0
- package/crates/jettypod-core/src/skills.rs +556 -0
- package/crates/jettypod-core/src/work.rs +1086 -0
- package/crates/jettypod-core/src/worktree.rs +628 -0
- package/crates/jettypod-core/src/ws.rs +767 -0
- package/cucumber-test.cjs +6 -0
- package/jettypod.js +96 -4
- package/lib/bdd-preflight.js +96 -0
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/work-commands/index.js +58 -16
- package/lib/work-tracking/index.js +108 -8
- package/package.json +1 -1
- package/skills-templates/bug-mode/SKILL.md +43 -1
- package/skills-templates/chore-mode/SKILL.md +40 -1
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +14 -0
- package/skills-templates/feature-planning/SKILL.md +90 -1
- package/skills-templates/production-mode/SKILL.md +20 -0
- package/skills-templates/simple-improvement/SKILL.md +39 -2
- package/skills-templates/speed-mode/SKILL.md +10 -15
- package/skills-templates/stable-mode/SKILL.md +47 -0
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -525
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
- package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
- package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
- package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
- package/apps/dashboard/app/api/kanban/route.ts +0 -15
- package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
- package/apps/dashboard/app/api/settings/general/route.ts +0 -21
- package/apps/dashboard/app/api/tests/route.ts +0 -9
- package/apps/dashboard/app/api/tests/run/route.ts +0 -82
- package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
- package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
- package/apps/dashboard/app/api/usage/route.ts +0 -17
- package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/route.ts +0 -35
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -55
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
- package/apps/dashboard/electron/ipc-handlers.js +0 -1026
- package/apps/dashboard/electron/main.js +0 -2306
- package/apps/dashboard/electron/preload.js +0 -125
- package/apps/dashboard/electron/session-manager.js +0 -163
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/backlog-parser.ts +0 -50
- package/apps/dashboard/lib/claude-process-manager.ts +0 -529
- package/apps/dashboard/lib/db-bridge.ts +0 -283
- package/apps/dashboard/lib/prototypes.ts +0 -202
- package/apps/dashboard/lib/test-results-db.ts +0 -307
- package/apps/dashboard/lib/tests.ts +0 -282
- package/apps/dashboard/next.config.js +0 -66
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/bug-icon.svg +0 -9
- package/apps/dashboard/public/buoy-icon.svg +0 -9
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/in-flight-seagull.svg +0 -9
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/pier-icon.svg +0 -14
- package/apps/dashboard/public/star-icon.svg +0 -9
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/public/wrench-icon.svg +0 -9
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
|
-
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
|
2
|
+
import { useState, useCallback, useRef, useEffect, useMemo, memo } from 'react';
|
|
3
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
4
4
|
import type { WorkItem, InFlightItem, KanbanGroup } from '@/lib/db';
|
|
5
5
|
import type { UndoAction } from '@/lib/undoStack';
|
|
6
|
-
import type { Session } from '../contexts/ClaudeSessionContext';
|
|
7
6
|
import { DragProvider } from './DragContext';
|
|
8
7
|
import { DraggableCard } from './DraggableCard';
|
|
9
8
|
import { DropZone } from './DropZone';
|
|
@@ -42,9 +41,10 @@ interface KanbanColumnProps {
|
|
|
42
41
|
count: number;
|
|
43
42
|
onAdd?: () => void;
|
|
44
43
|
addDisabled?: boolean;
|
|
44
|
+
scrollRef?: React.RefObject<HTMLDivElement | null>;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function KanbanColumn({ title, children, count, onAdd, addDisabled }: KanbanColumnProps) {
|
|
47
|
+
function KanbanColumn({ title, children, count, onAdd, addDisabled, scrollRef }: KanbanColumnProps) {
|
|
48
48
|
const testId = title.toLowerCase().replace(/\s+/g, '-') + '-column';
|
|
49
49
|
return (
|
|
50
50
|
<div className="flex-1 max-w-[600px] flex flex-col min-h-0" data-testid={testId}>
|
|
@@ -78,7 +78,7 @@ function KanbanColumn({ title, children, count, onAdd, addDisabled }: KanbanColu
|
|
|
78
78
|
</span>
|
|
79
79
|
</div>
|
|
80
80
|
</div>
|
|
81
|
-
<div className="overflow-y-auto flex-1 min-h-0 px-
|
|
81
|
+
<div ref={scrollRef} className="overflow-y-auto overflow-x-hidden flex-1 min-h-0 px-3 -mx-3" style={{ contain: 'paint' }}>
|
|
82
82
|
{children}
|
|
83
83
|
</div>
|
|
84
84
|
</div>
|
|
@@ -145,19 +145,20 @@ function BacklogDropZoneWrapper({ backlog, onStatusChange, onOrderChange, childr
|
|
|
145
145
|
const itemMap = new Map(backlogItems.map(item => [item.id, item]));
|
|
146
146
|
const visualOrder = cardPositions.map(pos => itemMap.get(pos.id)!).filter(Boolean);
|
|
147
147
|
|
|
148
|
-
// Calculate proper midpoint display_order between surrounding items
|
|
148
|
+
// Calculate proper midpoint display_order between surrounding items.
|
|
149
|
+
// Fallback uses id * INCREMENT to match the sort comparator and give proper gaps.
|
|
149
150
|
let newOrder: number;
|
|
150
151
|
if (visualOrder.length === 0) {
|
|
151
152
|
newOrder = DISPLAY_ORDER_INCREMENT;
|
|
152
153
|
} else if (insertIndex === 0) {
|
|
153
|
-
const firstOrder = visualOrder[0].display_order ?? visualOrder[0].id;
|
|
154
|
+
const firstOrder = visualOrder[0].display_order ?? visualOrder[0].id * DISPLAY_ORDER_INCREMENT;
|
|
154
155
|
newOrder = firstOrder - DISPLAY_ORDER_INCREMENT;
|
|
155
156
|
} else if (insertIndex >= visualOrder.length) {
|
|
156
|
-
const lastOrder = visualOrder[visualOrder.length - 1].display_order ?? visualOrder[visualOrder.length - 1].id;
|
|
157
|
+
const lastOrder = visualOrder[visualOrder.length - 1].display_order ?? visualOrder[visualOrder.length - 1].id * DISPLAY_ORDER_INCREMENT;
|
|
157
158
|
newOrder = lastOrder + DISPLAY_ORDER_INCREMENT;
|
|
158
159
|
} else {
|
|
159
|
-
const before = visualOrder[insertIndex - 1].display_order ?? visualOrder[insertIndex - 1].id;
|
|
160
|
-
const after = visualOrder[insertIndex].display_order ?? visualOrder[insertIndex].id;
|
|
160
|
+
const before = visualOrder[insertIndex - 1].display_order ?? visualOrder[insertIndex - 1].id * DISPLAY_ORDER_INCREMENT;
|
|
161
|
+
const after = visualOrder[insertIndex].display_order ?? visualOrder[insertIndex].id * DISPLAY_ORDER_INCREMENT;
|
|
161
162
|
newOrder = Math.floor((before + after) / 2);
|
|
162
163
|
}
|
|
163
164
|
|
|
@@ -175,8 +176,8 @@ function BacklogDropZoneWrapper({ backlog, onStatusChange, onOrderChange, childr
|
|
|
175
176
|
onReorder={handleBacklogReorder}
|
|
176
177
|
allowReorder={true}
|
|
177
178
|
className="rounded-lg p-3 -m-3 min-h-[100px]"
|
|
178
|
-
highlightClassName="ring-2 ring-
|
|
179
|
-
reorderHighlightClassName="ring-2 ring-
|
|
179
|
+
highlightClassName="ring-2 ring-[#819D9F] bg-[#E8EEEF]/50 dark:bg-[#819D9F]/20"
|
|
180
|
+
reorderHighlightClassName="ring-2 ring-[#E3D985] bg-[#F9F7E8]/50 dark:bg-[#E3D985]/20"
|
|
180
181
|
data-testid="backlog-drop-zone"
|
|
181
182
|
>
|
|
182
183
|
{children}
|
|
@@ -184,6 +185,153 @@ function BacklogDropZoneWrapper({ backlog, onStatusChange, onOrderChange, childr
|
|
|
184
185
|
);
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
// Virtualized list of EpicGroups within a column's scroll container.
|
|
189
|
+
// Each EpicGroup is a virtual row — off-screen groups are not rendered at all,
|
|
190
|
+
// saving DOM nodes and mount/unmount overhead.
|
|
191
|
+
type VirtualRow =
|
|
192
|
+
| { type: 'group'; key: string; group: KanbanGroup }
|
|
193
|
+
| { type: 'showMore' }
|
|
194
|
+
| { type: 'empty' };
|
|
195
|
+
|
|
196
|
+
interface VirtualizedEpicListProps {
|
|
197
|
+
entries: [string, KanbanGroup][];
|
|
198
|
+
scrollRef: React.RefObject<HTMLDivElement | null>;
|
|
199
|
+
scrollMargin?: number;
|
|
200
|
+
hasMore: boolean;
|
|
201
|
+
onShowMore: () => void;
|
|
202
|
+
emptyMessage?: string;
|
|
203
|
+
// EpicGroup props pass-through
|
|
204
|
+
inFlightByEpic?: Map<number, InFlightItem[]>;
|
|
205
|
+
isDraggable?: boolean;
|
|
206
|
+
onTitleSave?: (id: number, newTitle: string) => Promise<void>;
|
|
207
|
+
onStatusChange?: (id: number, newStatus: string) => Promise<void | { success: boolean; notFound?: boolean }>;
|
|
208
|
+
onReject?: (id: number, reason: string) => Promise<void>;
|
|
209
|
+
onRestart?: (id: number) => void;
|
|
210
|
+
onEpicAssign?: (id: number, epicId: number | null) => Promise<void>;
|
|
211
|
+
onOrderChange?: (id: number, newOrder: number) => Promise<void>;
|
|
212
|
+
onTriggerClaude?: (id: number, title: string, type: string, conversational?: boolean, description?: string | null) => void;
|
|
213
|
+
activeSessionIds?: Set<string>;
|
|
214
|
+
onOpenSession?: (id: string) => void;
|
|
215
|
+
onCloseSession?: (id: string) => void;
|
|
216
|
+
onError?: (message: string) => void;
|
|
217
|
+
usageAllowed?: boolean;
|
|
218
|
+
animatingItemId?: number | null;
|
|
219
|
+
onAnimationComplete?: () => void;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const VirtualizedEpicList = memo(function VirtualizedEpicList({
|
|
223
|
+
entries,
|
|
224
|
+
scrollRef,
|
|
225
|
+
scrollMargin = 0,
|
|
226
|
+
hasMore,
|
|
227
|
+
onShowMore,
|
|
228
|
+
emptyMessage,
|
|
229
|
+
inFlightByEpic,
|
|
230
|
+
isDraggable,
|
|
231
|
+
onTitleSave,
|
|
232
|
+
onStatusChange,
|
|
233
|
+
onReject,
|
|
234
|
+
onRestart,
|
|
235
|
+
onEpicAssign,
|
|
236
|
+
onOrderChange,
|
|
237
|
+
onTriggerClaude,
|
|
238
|
+
activeSessionIds,
|
|
239
|
+
onOpenSession,
|
|
240
|
+
onCloseSession,
|
|
241
|
+
onError,
|
|
242
|
+
usageAllowed,
|
|
243
|
+
animatingItemId,
|
|
244
|
+
onAnimationComplete,
|
|
245
|
+
}: VirtualizedEpicListProps) {
|
|
246
|
+
const rows = useMemo<VirtualRow[]>(() => {
|
|
247
|
+
const r: VirtualRow[] = [];
|
|
248
|
+
for (const [key, group] of entries) {
|
|
249
|
+
r.push({ type: 'group', key, group });
|
|
250
|
+
}
|
|
251
|
+
if (hasMore) r.push({ type: 'showMore' });
|
|
252
|
+
if (entries.length === 0 && emptyMessage) r.push({ type: 'empty' });
|
|
253
|
+
return r;
|
|
254
|
+
}, [entries, hasMore, emptyMessage]);
|
|
255
|
+
|
|
256
|
+
const virtualizer = useVirtualizer({
|
|
257
|
+
count: rows.length,
|
|
258
|
+
getScrollElement: () => scrollRef.current,
|
|
259
|
+
estimateSize: (index) => {
|
|
260
|
+
const row = rows[index];
|
|
261
|
+
if (!row || row.type === 'showMore' || row.type === 'empty') return 44;
|
|
262
|
+
// Estimate: header (~36px) + cards * ~95px per card + group padding
|
|
263
|
+
const isStandalone = !row.group.epicTitle && row.group.items.length === 1;
|
|
264
|
+
return isStandalone
|
|
265
|
+
? 82 + 8 // card height + spacing
|
|
266
|
+
: 36 + row.group.items.length * 95 + 24; // header + cards + spacing
|
|
267
|
+
},
|
|
268
|
+
scrollMargin,
|
|
269
|
+
overscan: 3,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (rows.length === 0) return null;
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<div style={{ height: virtualizer.getTotalSize(), position: 'relative', width: '100%' }}>
|
|
276
|
+
{virtualizer.getVirtualItems().map((virtualItem) => {
|
|
277
|
+
const row = rows[virtualItem.index];
|
|
278
|
+
// Bottom padding replaces EpicGroup's mb-* (margin is invisible with absolute positioning)
|
|
279
|
+
const isStandalone = row.type === 'group' && !row.group.epicTitle && row.group.items.length === 1;
|
|
280
|
+
const rowPaddingBottom = row.type === 'group' ? (isStandalone ? 8 : 24) : 0;
|
|
281
|
+
return (
|
|
282
|
+
<div
|
|
283
|
+
key={virtualItem.key}
|
|
284
|
+
data-index={virtualItem.index}
|
|
285
|
+
ref={virtualizer.measureElement}
|
|
286
|
+
style={{
|
|
287
|
+
position: 'absolute',
|
|
288
|
+
top: 0,
|
|
289
|
+
left: 0,
|
|
290
|
+
width: '100%',
|
|
291
|
+
transform: `translateY(${virtualItem.start - scrollMargin}px)`,
|
|
292
|
+
paddingBottom: rowPaddingBottom,
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
{row.type === 'group' ? (
|
|
296
|
+
<EpicGroup
|
|
297
|
+
epicId={row.group.epicId}
|
|
298
|
+
epicTitle={row.group.epicTitle}
|
|
299
|
+
items={row.group.items}
|
|
300
|
+
isInFlight={row.group.epicId ? inFlightByEpic?.has(row.group.epicId) : false}
|
|
301
|
+
inFlightItems={row.group.epicId ? inFlightByEpic?.get(row.group.epicId) : undefined}
|
|
302
|
+
isDraggable={isDraggable}
|
|
303
|
+
onTitleSave={onTitleSave}
|
|
304
|
+
onStatusChange={onStatusChange}
|
|
305
|
+
onReject={onReject}
|
|
306
|
+
onRestart={onRestart}
|
|
307
|
+
onEpicAssign={onEpicAssign}
|
|
308
|
+
onOrderChange={onOrderChange}
|
|
309
|
+
onTriggerClaude={onTriggerClaude}
|
|
310
|
+
activeSessionIds={activeSessionIds}
|
|
311
|
+
onOpenSession={onOpenSession}
|
|
312
|
+
onCloseSession={onCloseSession}
|
|
313
|
+
onError={onError}
|
|
314
|
+
usageAllowed={usageAllowed}
|
|
315
|
+
animatingItemId={animatingItemId}
|
|
316
|
+
onAnimationComplete={onAnimationComplete}
|
|
317
|
+
/>
|
|
318
|
+
) : row.type === 'showMore' ? (
|
|
319
|
+
<button
|
|
320
|
+
onClick={onShowMore}
|
|
321
|
+
className="w-full mt-3 py-2 text-sm text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 hover:bg-zinc-200/50 dark:hover:bg-zinc-800/50 rounded-lg transition-colors duration-200 ease-out"
|
|
322
|
+
>
|
|
323
|
+
Show more
|
|
324
|
+
</button>
|
|
325
|
+
) : (
|
|
326
|
+
<p className="text-base text-zinc-500 text-center py-4">{emptyMessage}</p>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
})}
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
187
335
|
interface KanbanBoardProps {
|
|
188
336
|
inFlight: InFlightItem[];
|
|
189
337
|
backlog: Map<string, KanbanGroup>;
|
|
@@ -198,7 +346,7 @@ interface KanbanBoardProps {
|
|
|
198
346
|
// Multi-session support
|
|
199
347
|
onOpenSession?: (id: string) => void;
|
|
200
348
|
onCloseSession?: (id: string) => void;
|
|
201
|
-
|
|
349
|
+
activeSessionIds?: Set<string>;
|
|
202
350
|
// Undo/redo support
|
|
203
351
|
onUndo?: () => Promise<UndoAction | null>;
|
|
204
352
|
onRedo?: () => Promise<UndoAction | null>;
|
|
@@ -206,6 +354,8 @@ interface KanbanBoardProps {
|
|
|
206
354
|
canRedo?: boolean;
|
|
207
355
|
// Error handler for drag-drop operations
|
|
208
356
|
onError?: (message: string) => void;
|
|
357
|
+
// Pre-built status map from data-bridge (avoids O(N) rebuild per render)
|
|
358
|
+
itemStatusMap?: Map<number, string>;
|
|
209
359
|
// Add to backlog
|
|
210
360
|
onAddToBacklog?: () => void;
|
|
211
361
|
// Usage limits
|
|
@@ -215,10 +365,28 @@ interface KanbanBoardProps {
|
|
|
215
365
|
onExternalAnimationComplete?: () => void;
|
|
216
366
|
}
|
|
217
367
|
|
|
218
|
-
export function KanbanBoard({ inFlight, backlog, done, onTitleSave, onStatusChange, onReject, onRestart, onOrderChange, onEpicAssign, onTriggerClaude, onOpenSession, onCloseSession,
|
|
368
|
+
export const KanbanBoard = memo(function KanbanBoard({ inFlight, backlog, done, onTitleSave, onStatusChange, onReject, onRestart, onOrderChange, onEpicAssign, onTriggerClaude, onOpenSession, onCloseSession, activeSessionIds, onUndo, onRedo, canUndo, canRedo, onError, itemStatusMap: externalStatusMap, onAddToBacklog, usageAllowed = true, externalAnimatingItemId, onExternalAnimationComplete }: KanbanBoardProps) {
|
|
219
369
|
const backlogCount = inFlight.length + Array.from(backlog.values()).reduce((sum, g) => sum + g.items.length, 0);
|
|
220
370
|
const doneCount = Array.from(done.values()).reduce((sum, g) => sum + g.items.length, 0);
|
|
221
371
|
|
|
372
|
+
// Scroll container refs for virtualized columns
|
|
373
|
+
const backlogScrollRef = useRef<HTMLDivElement>(null);
|
|
374
|
+
const doneScrollRef = useRef<HTMLDivElement>(null);
|
|
375
|
+
|
|
376
|
+
// Measure non-virtualized content above the backlog virtualizer (In Flight + divider)
|
|
377
|
+
// so the virtualizer knows the correct scroll offset
|
|
378
|
+
const preBacklogRef = useRef<HTMLDivElement>(null);
|
|
379
|
+
const [backlogScrollMargin, setBacklogScrollMargin] = useState(0);
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
const el = preBacklogRef.current;
|
|
382
|
+
if (!el) return;
|
|
383
|
+
const ro = new ResizeObserver((entries) => {
|
|
384
|
+
setBacklogScrollMargin(entries[0]?.borderBoxSize?.[0]?.blockSize ?? el.offsetHeight);
|
|
385
|
+
});
|
|
386
|
+
ro.observe(el);
|
|
387
|
+
return () => ro.disconnect();
|
|
388
|
+
}, []);
|
|
389
|
+
|
|
222
390
|
// Lazy loading state for backlog and done columns
|
|
223
391
|
const [showAllBacklog, setShowAllBacklog] = useState(false);
|
|
224
392
|
const [showAllDone, setShowAllDone] = useState(false);
|
|
@@ -226,8 +394,14 @@ export function KanbanBoard({ inFlight, backlog, done, onTitleSave, onStatusChan
|
|
|
226
394
|
const backlogEntries = useMemo(() => Array.from(backlog.entries()), [backlog]);
|
|
227
395
|
const doneEntries = useMemo(() => Array.from(done.entries()), [done]);
|
|
228
396
|
const backlogLimit = Math.max(0, BACKLOG_VISIBLE_LIMIT - inFlight.length);
|
|
229
|
-
const { visible: visibleBacklog, hasMore: hasMoreBacklog } =
|
|
230
|
-
|
|
397
|
+
const { visible: visibleBacklog, hasMore: hasMoreBacklog } = useMemo(
|
|
398
|
+
() => getVisibleEntries(backlogEntries, backlogLimit, showAllBacklog),
|
|
399
|
+
[backlogEntries, backlogLimit, showAllBacklog]
|
|
400
|
+
);
|
|
401
|
+
const { visible: visibleDone, hasMore: hasMoreDone } = useMemo(
|
|
402
|
+
() => getVisibleEntries(doneEntries, DONE_VISIBLE_LIMIT, showAllDone),
|
|
403
|
+
[doneEntries, showAllDone]
|
|
404
|
+
);
|
|
231
405
|
|
|
232
406
|
// Keyboard shortcuts for undo/redo (Cmd+Z / Cmd+Shift+Z)
|
|
233
407
|
useEffect(() => {
|
|
@@ -262,18 +436,21 @@ export function KanbanBoard({ inFlight, backlog, done, onTitleSave, onStatusChan
|
|
|
262
436
|
}, [onUndo, onRedo, canUndo, canRedo]);
|
|
263
437
|
|
|
264
438
|
// Build a map of epic IDs to their in-flight items
|
|
265
|
-
const inFlightByEpic =
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
existing
|
|
272
|
-
|
|
273
|
-
|
|
439
|
+
const inFlightByEpic = useMemo(() => {
|
|
440
|
+
const map = new Map<number, InFlightItem[]>();
|
|
441
|
+
for (const item of inFlight) {
|
|
442
|
+
const epicId = item.parent_id || item.epic_id;
|
|
443
|
+
if (epicId) {
|
|
444
|
+
const existing = map.get(epicId);
|
|
445
|
+
if (existing) {
|
|
446
|
+
existing.push(item);
|
|
447
|
+
} else {
|
|
448
|
+
map.set(epicId, [item]);
|
|
449
|
+
}
|
|
274
450
|
}
|
|
275
451
|
}
|
|
276
|
-
|
|
452
|
+
return map;
|
|
453
|
+
}, [inFlight]);
|
|
277
454
|
|
|
278
455
|
// Board-level animation state - tracks which item is playing the completion animation
|
|
279
456
|
const [internalAnimatingItemId, setInternalAnimatingItemId] = useState<number | null>(null);
|
|
@@ -282,24 +459,8 @@ export function KanbanBoard({ inFlight, backlog, done, onTitleSave, onStatusChan
|
|
|
282
459
|
// Merge internal (UI-driven) and external (CLI/DB-driven) animation triggers
|
|
283
460
|
const animatingItemId = internalAnimatingItemId ?? externalAnimatingItemId ?? null;
|
|
284
461
|
|
|
285
|
-
//
|
|
286
|
-
const itemStatusMap =
|
|
287
|
-
const map = new Map<number, string>();
|
|
288
|
-
for (const item of inFlight) {
|
|
289
|
-
map.set(item.id, item.status);
|
|
290
|
-
}
|
|
291
|
-
for (const group of backlog.values()) {
|
|
292
|
-
for (const item of group.items) {
|
|
293
|
-
map.set(item.id, item.status);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
for (const group of done.values()) {
|
|
297
|
-
for (const item of group.items) {
|
|
298
|
-
map.set(item.id, item.status);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
return map;
|
|
302
|
-
}, [inFlight, backlog, done]);
|
|
462
|
+
// Use pre-built statusMap from data-bridge when available (avoids O(N) rebuild)
|
|
463
|
+
const itemStatusMap = externalStatusMap ?? new Map<number, string>();
|
|
303
464
|
|
|
304
465
|
// Wrapper for onStatusChange that intercepts "done" transitions to play animation first
|
|
305
466
|
const handleStatusChangeWithAnimation = useCallback(async (id: number, newStatus: string) => {
|
|
@@ -309,6 +470,12 @@ export function KanbanBoard({ inFlight, backlog, done, onTitleSave, onStatusChan
|
|
|
309
470
|
|
|
310
471
|
// If transitioning to done from non-done status, play animation first in backlog/in-flight
|
|
311
472
|
if (newStatus === 'done' && currentStatus !== 'done') {
|
|
473
|
+
// Flush any existing pending change before replacing (rapid acceptance race)
|
|
474
|
+
if (pendingStatusChangeRef.current) {
|
|
475
|
+
const { id: prevId, status: prevStatus } = pendingStatusChangeRef.current;
|
|
476
|
+
pendingStatusChangeRef.current = null;
|
|
477
|
+
onStatusChange(prevId, prevStatus);
|
|
478
|
+
}
|
|
312
479
|
pendingStatusChangeRef.current = { id, status: newStatus };
|
|
313
480
|
setInternalAnimatingItemId(id);
|
|
314
481
|
return;
|
|
@@ -334,10 +501,18 @@ export function KanbanBoard({ inFlight, backlog, done, onTitleSave, onStatusChan
|
|
|
334
501
|
}
|
|
335
502
|
}, [onStatusChange, internalAnimatingItemId, externalAnimatingItemId, onExternalAnimationComplete]);
|
|
336
503
|
|
|
504
|
+
// Map for O(1) lookup in drag overlay instead of O(N) find
|
|
505
|
+
const inFlightMap = useMemo(() => {
|
|
506
|
+
const map = new Map<number, InFlightItem>();
|
|
507
|
+
for (const item of inFlight) {
|
|
508
|
+
map.set(item.id, item);
|
|
509
|
+
}
|
|
510
|
+
return map;
|
|
511
|
+
}, [inFlight]);
|
|
512
|
+
|
|
337
513
|
// Render function for the drag overlay
|
|
338
514
|
const renderDragOverlay = useCallback((item: WorkItem) => {
|
|
339
|
-
|
|
340
|
-
const inFlightItem = inFlight.find(i => i.id === item.id);
|
|
515
|
+
const inFlightItem = inFlightMap.get(item.id);
|
|
341
516
|
const epicTitle = inFlightItem?.epicTitle || null;
|
|
342
517
|
const isInFlightCard = inFlightItem !== undefined;
|
|
343
518
|
|
|
@@ -349,113 +524,99 @@ export function KanbanBoard({ inFlight, backlog, done, onTitleSave, onStatusChan
|
|
|
349
524
|
isInFlight={isInFlightCard}
|
|
350
525
|
/>
|
|
351
526
|
);
|
|
352
|
-
}, [
|
|
527
|
+
}, [inFlightMap]);
|
|
353
528
|
|
|
354
529
|
return (
|
|
355
530
|
<DragProvider renderDragOverlay={renderDragOverlay} onRemoveFromEpic={onEpicAssign} onError={onError}>
|
|
356
|
-
|
|
531
|
+
{/* height: --main-h (from AppShell ResizeObserver) minus page py-4 padding */}
|
|
532
|
+
<div className="flex gap-4 overflow-x-auto" style={{ height: 'calc(var(--main-h, 100vh) - 2rem)' }} data-testid="kanban-board">
|
|
357
533
|
{/* Backlog Column */}
|
|
358
|
-
<KanbanColumn title="Backlog" count={backlogCount} onAdd={onAddToBacklog} addDisabled={!usageAllowed}>
|
|
359
|
-
{/* In Flight
|
|
360
|
-
<
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
<
|
|
534
|
+
<KanbanColumn title="Backlog" count={backlogCount} onAdd={onAddToBacklog} addDisabled={!usageAllowed} scrollRef={backlogScrollRef}>
|
|
535
|
+
{/* Non-virtualized section: In Flight + divider (measured for scrollMargin) */}
|
|
536
|
+
<div ref={preBacklogRef}>
|
|
537
|
+
{/* In Flight Section - Drop Zone */}
|
|
538
|
+
<DropZone
|
|
539
|
+
targetStatus="in_progress"
|
|
540
|
+
onDrop={async (itemId, newStatus) => {
|
|
541
|
+
await handleStatusChangeWithAnimation(itemId, newStatus);
|
|
542
|
+
}}
|
|
543
|
+
className="rounded-lg mb-6 p-3 -m-3"
|
|
544
|
+
highlightClassName="ring-2 ring-[#819D9F] bg-[#819D9F]/10 dark:bg-[#819D9F]/20"
|
|
545
|
+
data-testid="in-flight-drop-zone"
|
|
546
|
+
>
|
|
547
|
+
{inFlight.length > 0 ? (
|
|
548
|
+
<div data-testid="in-flight-section" className="bg-[#e8f0f0] dark:bg-[#819D9F]/20 rounded-lg p-3 -m-1">
|
|
549
|
+
<div className="flex items-center gap-2 text-base font-medium text-[#5a7d7f] dark:text-[#a3bfc0] mb-3">
|
|
550
|
+
<img src="/in-flight-seagull.png" alt="" className="w-6 h-6 object-contain" />
|
|
551
|
+
<span>In Flight</span>
|
|
552
|
+
</div>
|
|
553
|
+
<div className="space-y-3">
|
|
554
|
+
{inFlight.map((item) => (
|
|
555
|
+
<DraggableCard key={item.id} item={item}>
|
|
556
|
+
<KanbanCard
|
|
557
|
+
item={item}
|
|
558
|
+
epicTitle={item.epicTitle}
|
|
559
|
+
showEpic={true}
|
|
560
|
+
isInFlight={true}
|
|
561
|
+
onTitleSave={onTitleSave}
|
|
562
|
+
onStatusChange={handleStatusChangeWithAnimation}
|
|
563
|
+
onReject={onReject}
|
|
564
|
+
onRestart={onRestart}
|
|
565
|
+
onTriggerClaude={onTriggerClaude}
|
|
566
|
+
hasActiveSession={activeSessionIds?.has(String(item.id))}
|
|
567
|
+
onOpenSession={onOpenSession}
|
|
568
|
+
onCloseSession={onCloseSession}
|
|
569
|
+
usageAllowed={usageAllowed}
|
|
570
|
+
isCompletingAnimation={animatingItemId === item.id}
|
|
571
|
+
onAnimationComplete={handleAnimationComplete}
|
|
572
|
+
/>
|
|
573
|
+
</DraggableCard>
|
|
574
|
+
))}
|
|
575
|
+
</div>
|
|
374
576
|
</div>
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
item={item}
|
|
380
|
-
epicTitle={item.epicTitle}
|
|
381
|
-
showEpic={true}
|
|
382
|
-
isInFlight={true}
|
|
383
|
-
onTitleSave={onTitleSave}
|
|
384
|
-
onStatusChange={handleStatusChangeWithAnimation}
|
|
385
|
-
onReject={onReject}
|
|
386
|
-
onRestart={onRestart}
|
|
387
|
-
onTriggerClaude={onTriggerClaude}
|
|
388
|
-
hasActiveSession={activeSessions?.has(String(item.id))}
|
|
389
|
-
onOpenSession={onOpenSession}
|
|
390
|
-
onCloseSession={onCloseSession}
|
|
391
|
-
usageAllowed={usageAllowed}
|
|
392
|
-
isCompletingAnimation={animatingItemId === item.id}
|
|
393
|
-
onAnimationComplete={handleAnimationComplete}
|
|
394
|
-
/>
|
|
395
|
-
</DraggableCard>
|
|
396
|
-
))}
|
|
577
|
+
) : (
|
|
578
|
+
<div className="flex items-center gap-2 text-base font-medium text-zinc-400 dark:text-zinc-500 py-3">
|
|
579
|
+
<img src="/in-flight-seagull.png" alt="" className="w-6 h-6 object-contain opacity-50" />
|
|
580
|
+
<span>Drop here to start work</span>
|
|
397
581
|
</div>
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
<div className="flex items-center gap-2 text-base font-medium text-zinc-400 dark:text-zinc-500 py-3">
|
|
401
|
-
<img src="/in-flight-seagull.svg" alt="" className="w-5 h-5 opacity-50" />
|
|
402
|
-
<span>Drop here to start work</span>
|
|
403
|
-
</div>
|
|
404
|
-
)}
|
|
405
|
-
</DropZone>
|
|
582
|
+
)}
|
|
583
|
+
</DropZone>
|
|
406
584
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
585
|
+
{/* Divider if both sections have content */}
|
|
586
|
+
{(inFlight.length > 0 || backlog.size > 0) && (
|
|
587
|
+
<hr className="border-zinc-300 dark:border-zinc-700 my-6" />
|
|
588
|
+
)}
|
|
589
|
+
</div>
|
|
411
590
|
|
|
412
|
-
{/* Backlog Section - Drop Zone with Reordering */}
|
|
591
|
+
{/* Virtualized Backlog Section - Drop Zone with Reordering */}
|
|
413
592
|
<BacklogDropZoneWrapper
|
|
414
593
|
backlog={backlog}
|
|
415
594
|
onStatusChange={handleStatusChangeWithAnimation}
|
|
416
595
|
onOrderChange={onOrderChange}
|
|
417
596
|
>
|
|
418
|
-
<
|
|
419
|
-
{
|
|
420
|
-
{
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
onAnimationComplete={handleAnimationComplete}
|
|
442
|
-
/>
|
|
443
|
-
))}
|
|
444
|
-
|
|
445
|
-
{hasMoreBacklog && (
|
|
446
|
-
<button
|
|
447
|
-
onClick={() => setShowAllBacklog(true)}
|
|
448
|
-
className="w-full mt-3 py-2 text-sm text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 hover:bg-zinc-200/50 dark:hover:bg-zinc-800/50 rounded-lg transition-colors duration-200 ease-out"
|
|
449
|
-
data-testid="backlog-show-more"
|
|
450
|
-
>
|
|
451
|
-
Show more
|
|
452
|
-
</button>
|
|
453
|
-
)}
|
|
454
|
-
|
|
455
|
-
{backlog.size === 0 && (
|
|
456
|
-
<p className="text-base text-zinc-500 text-center py-4">Drop items here for backlog</p>
|
|
457
|
-
)}
|
|
458
|
-
</div>
|
|
597
|
+
<VirtualizedEpicList
|
|
598
|
+
entries={visibleBacklog}
|
|
599
|
+
scrollRef={backlogScrollRef}
|
|
600
|
+
scrollMargin={backlogScrollMargin}
|
|
601
|
+
hasMore={hasMoreBacklog}
|
|
602
|
+
onShowMore={() => setShowAllBacklog(true)}
|
|
603
|
+
emptyMessage={backlog.size === 0 ? 'Drop items here for backlog' : undefined}
|
|
604
|
+
inFlightByEpic={inFlightByEpic}
|
|
605
|
+
onTitleSave={onTitleSave}
|
|
606
|
+
onStatusChange={handleStatusChangeWithAnimation}
|
|
607
|
+
onReject={onReject}
|
|
608
|
+
onRestart={onRestart}
|
|
609
|
+
onEpicAssign={onEpicAssign}
|
|
610
|
+
onOrderChange={onOrderChange}
|
|
611
|
+
onTriggerClaude={onTriggerClaude}
|
|
612
|
+
activeSessionIds={activeSessionIds}
|
|
613
|
+
onOpenSession={onOpenSession}
|
|
614
|
+
onCloseSession={onCloseSession}
|
|
615
|
+
onError={onError}
|
|
616
|
+
usageAllowed={usageAllowed}
|
|
617
|
+
animatingItemId={animatingItemId}
|
|
618
|
+
onAnimationComplete={handleAnimationComplete}
|
|
619
|
+
/>
|
|
459
620
|
</BacklogDropZoneWrapper>
|
|
460
621
|
|
|
461
622
|
{backlogCount === 0 && inFlight.length === 0 && (
|
|
@@ -464,7 +625,7 @@ export function KanbanBoard({ inFlight, backlog, done, onTitleSave, onStatusChan
|
|
|
464
625
|
</KanbanColumn>
|
|
465
626
|
|
|
466
627
|
{/* Done Column */}
|
|
467
|
-
<KanbanColumn title="Done" count={doneCount}>
|
|
628
|
+
<KanbanColumn title="Done" count={doneCount} scrollRef={doneScrollRef}>
|
|
468
629
|
<DropZone
|
|
469
630
|
targetStatus="done"
|
|
470
631
|
onDrop={async (itemId, newStatus) => {
|
|
@@ -474,40 +635,25 @@ export function KanbanBoard({ inFlight, backlog, done, onTitleSave, onStatusChan
|
|
|
474
635
|
highlightClassName="ring-2 ring-zinc-400 bg-zinc-100/50 dark:bg-zinc-800/50"
|
|
475
636
|
data-testid="done-drop-zone"
|
|
476
637
|
>
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
))}
|
|
494
|
-
|
|
495
|
-
{hasMoreDone && (
|
|
496
|
-
<button
|
|
497
|
-
onClick={() => setShowAllDone(true)}
|
|
498
|
-
className="w-full mt-3 py-2 text-sm text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 hover:bg-zinc-200/50 dark:hover:bg-zinc-800/50 rounded-lg transition-colors duration-200 ease-out"
|
|
499
|
-
data-testid="done-show-more"
|
|
500
|
-
>
|
|
501
|
-
Show more
|
|
502
|
-
</button>
|
|
503
|
-
)}
|
|
504
|
-
|
|
505
|
-
{doneCount === 0 && (
|
|
506
|
-
<p className="text-base text-zinc-500 text-center py-4">Drop here to mark complete</p>
|
|
507
|
-
)}
|
|
638
|
+
<VirtualizedEpicList
|
|
639
|
+
entries={visibleDone}
|
|
640
|
+
scrollRef={doneScrollRef}
|
|
641
|
+
hasMore={hasMoreDone}
|
|
642
|
+
onShowMore={() => setShowAllDone(true)}
|
|
643
|
+
emptyMessage={doneCount === 0 ? 'Drop here to mark complete' : undefined}
|
|
644
|
+
isDraggable={true}
|
|
645
|
+
onTitleSave={onTitleSave}
|
|
646
|
+
onStatusChange={handleStatusChangeWithAnimation}
|
|
647
|
+
onReject={onReject}
|
|
648
|
+
activeSessionIds={activeSessionIds}
|
|
649
|
+
onOpenSession={onOpenSession}
|
|
650
|
+
onCloseSession={onCloseSession}
|
|
651
|
+
onError={onError}
|
|
652
|
+
usageAllowed={usageAllowed}
|
|
653
|
+
/>
|
|
508
654
|
</DropZone>
|
|
509
655
|
</KanbanColumn>
|
|
510
656
|
</div>
|
|
511
657
|
</DragProvider>
|
|
512
658
|
);
|
|
513
|
-
}
|
|
659
|
+
});
|