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.
Files changed (208) hide show
  1. package/.env +2 -1
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +54 -49
  8. package/apps/dashboard/app/demo/gates/page.tsx +3 -5
  9. package/apps/dashboard/app/design-system/page.tsx +1 -1
  10. package/apps/dashboard/app/globals.css +74 -2
  11. package/apps/dashboard/app/install-claude/page.tsx +3 -5
  12. package/apps/dashboard/app/login/page.tsx +17 -20
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +60 -12
  15. package/apps/dashboard/app/signup/page.tsx +14 -17
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +12 -15
  19. package/apps/dashboard/app/work/[id]/page.tsx +90 -75
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +70 -61
  22. package/apps/dashboard/components/CardMenu.tsx +0 -1
  23. package/apps/dashboard/components/ClaudePanel.tsx +541 -283
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
  26. package/apps/dashboard/components/CopyableId.tsx +1 -2
  27. package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
  28. package/apps/dashboard/components/DragContext.tsx +132 -62
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +5 -6
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +0 -1
  34. package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
  35. package/apps/dashboard/components/EpicGroup.tsx +100 -70
  36. package/apps/dashboard/components/GateCard.tsx +0 -1
  37. package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
  39. package/apps/dashboard/components/JettyLoader.tsx +0 -1
  40. package/apps/dashboard/components/KanbanBoard.tsx +319 -173
  41. package/apps/dashboard/components/KanbanCard.tsx +341 -107
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
  44. package/apps/dashboard/components/MainNav.tsx +24 -25
  45. package/apps/dashboard/components/MessageBlock.tsx +93 -16
  46. package/apps/dashboard/components/ModeStartCard.tsx +0 -1
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
  48. package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
  53. package/apps/dashboard/components/ReviewFooter.tsx +12 -14
  54. package/apps/dashboard/components/SessionList.tsx +0 -1
  55. package/apps/dashboard/components/SubscribeContent.tsx +40 -11
  56. package/apps/dashboard/components/TestTree.tsx +1 -2
  57. package/apps/dashboard/components/TipCard.tsx +2 -4
  58. package/apps/dashboard/components/Toast.tsx +0 -1
  59. package/apps/dashboard/components/TypeIcon.tsx +7 -8
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
  62. package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
  63. package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
  64. package/apps/dashboard/components/WorkItemTree.tsx +2 -4
  65. package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +20 -73
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
  72. package/apps/dashboard/components/ui/Button.tsx +1 -1
  73. package/apps/dashboard/components/ui/Input.tsx +1 -1
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
  77. package/apps/dashboard/contexts/UsageContext.tsx +62 -31
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  81. package/apps/dashboard/index.html +73 -0
  82. package/apps/dashboard/lib/data-bridge.ts +722 -0
  83. package/apps/dashboard/lib/db.ts +69 -1302
  84. package/apps/dashboard/lib/environment-config.ts +173 -0
  85. package/apps/dashboard/lib/environment-verification.ts +119 -0
  86. package/apps/dashboard/lib/kanban-utils.ts +226 -26
  87. package/apps/dashboard/lib/proof-run.ts +495 -0
  88. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  89. package/apps/dashboard/lib/service-recovery.ts +326 -0
  90. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  91. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  92. package/apps/dashboard/lib/session-stream-manager.ts +253 -122
  93. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  94. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  95. package/apps/dashboard/lib/tauri.ts +106 -0
  96. package/apps/dashboard/lib/utils.ts +3 -3
  97. package/apps/dashboard/next-env.d.ts +1 -1
  98. package/apps/dashboard/package.json +21 -33
  99. package/apps/dashboard/public/bug-icon.png +0 -0
  100. package/apps/dashboard/public/buoy-icon.png +0 -0
  101. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  102. package/apps/dashboard/public/pier-icon.png +0 -0
  103. package/apps/dashboard/public/star-icon.png +0 -0
  104. package/apps/dashboard/public/wrench-icon.png +0 -0
  105. package/apps/dashboard/scripts/tauri-build.js +228 -0
  106. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  107. package/apps/dashboard/src/main.tsx +12 -0
  108. package/apps/dashboard/src/router.tsx +107 -0
  109. package/apps/dashboard/src/vite-env.d.ts +1 -0
  110. package/apps/dashboard/tsconfig.json +7 -12
  111. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  112. package/apps/dashboard/vite.config.ts +33 -0
  113. package/apps/update-server/src/index.ts +167 -30
  114. package/claude-hooks/global-guardrails.js +14 -13
  115. package/crates/jettypod-cli/Cargo.toml +19 -0
  116. package/crates/jettypod-cli/src/commands.rs +1249 -0
  117. package/crates/jettypod-cli/src/main.rs +595 -0
  118. package/crates/jettypod-core/Cargo.toml +26 -0
  119. package/crates/jettypod-core/build.rs +98 -0
  120. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  121. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  122. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  123. package/crates/jettypod-core/src/auth.rs +294 -0
  124. package/crates/jettypod-core/src/config.rs +397 -0
  125. package/crates/jettypod-core/src/db/mod.rs +507 -0
  126. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  127. package/crates/jettypod-core/src/db/startup.rs +101 -0
  128. package/crates/jettypod-core/src/db/validate.rs +149 -0
  129. package/crates/jettypod-core/src/error.rs +76 -0
  130. package/crates/jettypod-core/src/git.rs +458 -0
  131. package/crates/jettypod-core/src/lib.rs +20 -0
  132. package/crates/jettypod-core/src/sessions.rs +625 -0
  133. package/crates/jettypod-core/src/skills.rs +556 -0
  134. package/crates/jettypod-core/src/work.rs +1086 -0
  135. package/crates/jettypod-core/src/worktree.rs +628 -0
  136. package/crates/jettypod-core/src/ws.rs +767 -0
  137. package/cucumber-test.cjs +6 -0
  138. package/jettypod.js +96 -4
  139. package/lib/bdd-preflight.js +96 -0
  140. package/lib/merge-lock.js +111 -253
  141. package/lib/migrations/030-rejection-round-columns.js +54 -0
  142. package/lib/migrations/031-session-isolation-index.js +17 -0
  143. package/lib/work-commands/index.js +58 -16
  144. package/lib/work-tracking/index.js +108 -8
  145. package/package.json +1 -1
  146. package/skills-templates/bug-mode/SKILL.md +43 -1
  147. package/skills-templates/chore-mode/SKILL.md +40 -1
  148. package/skills-templates/design-system-selection/SKILL.md +273 -0
  149. package/skills-templates/epic-planning/SKILL.md +14 -0
  150. package/skills-templates/feature-planning/SKILL.md +90 -1
  151. package/skills-templates/production-mode/SKILL.md +20 -0
  152. package/skills-templates/simple-improvement/SKILL.md +39 -2
  153. package/skills-templates/speed-mode/SKILL.md +10 -15
  154. package/skills-templates/stable-mode/SKILL.md +47 -0
  155. package/apps/dashboard/README.md +0 -36
  156. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
  157. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  158. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
  159. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  160. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -525
  161. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  162. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  163. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  164. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  165. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  166. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  167. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  168. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  169. package/apps/dashboard/app/api/tests/route.ts +0 -9
  170. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  171. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  172. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  173. package/apps/dashboard/app/api/usage/route.ts +0 -17
  174. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  175. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  176. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  177. package/apps/dashboard/app/api/work/[id]/route.ts +0 -35
  178. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
  179. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  180. package/apps/dashboard/app/layout.tsx +0 -55
  181. package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
  182. package/apps/dashboard/electron/ipc-handlers.js +0 -1026
  183. package/apps/dashboard/electron/main.js +0 -2306
  184. package/apps/dashboard/electron/preload.js +0 -125
  185. package/apps/dashboard/electron/session-manager.js +0 -163
  186. package/apps/dashboard/electron-builder.config.js +0 -357
  187. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  188. package/apps/dashboard/lib/backlog-parser.ts +0 -50
  189. package/apps/dashboard/lib/claude-process-manager.ts +0 -529
  190. package/apps/dashboard/lib/db-bridge.ts +0 -283
  191. package/apps/dashboard/lib/prototypes.ts +0 -202
  192. package/apps/dashboard/lib/test-results-db.ts +0 -307
  193. package/apps/dashboard/lib/tests.ts +0 -282
  194. package/apps/dashboard/next.config.js +0 -66
  195. package/apps/dashboard/postcss.config.mjs +0 -7
  196. package/apps/dashboard/public/bug-icon.svg +0 -9
  197. package/apps/dashboard/public/buoy-icon.svg +0 -9
  198. package/apps/dashboard/public/file.svg +0 -1
  199. package/apps/dashboard/public/globe.svg +0 -1
  200. package/apps/dashboard/public/in-flight-seagull.svg +0 -9
  201. package/apps/dashboard/public/next.svg +0 -1
  202. package/apps/dashboard/public/pier-icon.svg +0 -14
  203. package/apps/dashboard/public/star-icon.svg +0 -9
  204. package/apps/dashboard/public/vercel.svg +0 -1
  205. package/apps/dashboard/public/window.svg +0 -1
  206. package/apps/dashboard/public/wrench-icon.svg +0 -9
  207. package/apps/dashboard/scripts/download-node.js +0 -104
  208. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
@@ -1,15 +1,10 @@
1
- 'use client';
2
-
3
- import { useState, useCallback, useRef, useEffect } from 'react';
4
- import Link from 'next/link';
5
- import { AnimatePresence } from 'framer-motion';
1
+ import { useCallback, useRef, useEffect, useState, memo } from 'react';
2
+ import { Link } from 'react-router-dom';
6
3
  import { useDroppable } from '@dnd-kit/core';
7
4
  import type { WorkItem, InFlightItem } from '@/lib/db';
8
- import type { Session } from '../contexts/ClaudeSessionContext';
9
5
  import { KanbanCard } from './KanbanCard';
10
6
  import { useDragContext } from './DragContext';
11
7
  import { DraggableCard } from './DraggableCard';
12
- import { PlaceholderCard } from './PlaceholderCard';
13
8
  import { TypeIcon } from './TypeIcon';
14
9
 
15
10
  // Safe bounds for display_order to prevent overflow
@@ -31,7 +26,7 @@ export interface EpicGroupProps {
31
26
  onEpicAssign?: (id: number, epicId: number | null) => Promise<void>;
32
27
  onOrderChange?: (id: number, newOrder: number) => Promise<void>;
33
28
  onTriggerClaude?: (id: number, title: string, type: string, conversational?: boolean, description?: string | null) => void;
34
- activeSessions?: Map<string, Session>;
29
+ activeSessionIds?: Set<string>;
35
30
  onOpenSession?: (id: string) => void;
36
31
  onCloseSession?: (id: string) => void;
37
32
  onError?: (message: string) => void;
@@ -41,19 +36,9 @@ export interface EpicGroupProps {
41
36
  onAnimationComplete?: () => void;
42
37
  }
43
38
 
44
- export function EpicGroup({ epicId, epicTitle, items, isInFlight = false, inFlightItems, isDraggable = true, onTitleSave, onStatusChange, onReject, onRestart, onEpicAssign, onOrderChange, onTriggerClaude, activeSessions, onOpenSession, onCloseSession, onError, usageAllowed = true, animatingItemId, onAnimationComplete }: EpicGroupProps) {
39
+ export const EpicGroup = memo(function EpicGroup({ epicId, epicTitle, items, isInFlight = false, inFlightItems, isDraggable = true, onTitleSave, onStatusChange, onReject, onRestart, onEpicAssign, onOrderChange, onTriggerClaude, activeSessionIds, onOpenSession, onCloseSession, onError, usageAllowed = true, animatingItemId, onAnimationComplete }: EpicGroupProps) {
45
40
  const containerRef = useRef<HTMLDivElement>(null);
46
- const { isDragging, draggedItem, activeEpicZone, activeDropZone, registerEpicDropZone, unregisterEpicDropZone, getCardPositions } = useDragContext();
47
-
48
- // Local pointer tracking - only this component needs pointer Y for insertion preview.
49
- // Using local state avoids re-rendering every context consumer at 60fps.
50
- const [pointerY, setPointerY] = useState(0);
51
- useEffect(() => {
52
- if (!isDragging) return;
53
- const onPointerMove = (e: PointerEvent) => { setPointerY(e.clientY); };
54
- window.addEventListener('pointermove', onPointerMove);
55
- return () => window.removeEventListener('pointermove', onPointerMove);
56
- }, [isDragging]);
41
+ const { isDragging, draggedItem, activeEpicZone, activeDropZone, registerEpicDropZone, unregisterEpicDropZone, getCardPositions, pointerPositionRef } = useDragContext();
57
42
 
58
43
  // Use @dnd-kit's useDroppable for epic zone collision detection
59
44
  const zoneId = epicId !== null ? `epic-${epicId}` : undefined;
@@ -122,19 +107,20 @@ export function EpicGroup({ epicId, epicTitle, items, isInFlight = false, inFlig
122
107
  const itemMap = new Map(currentItems.map(item => [item.id, item]));
123
108
  const visualOrder = cardPositions.map(pos => itemMap.get(pos.id)!).filter(Boolean);
124
109
 
125
- // Calculate proper midpoint display_order between surrounding items
110
+ // Calculate proper midpoint display_order between surrounding items.
111
+ // Fallback uses id * INCREMENT to match the sort comparator and give proper gaps.
126
112
  let newOrder: number;
127
113
  if (visualOrder.length === 0) {
128
114
  newOrder = DISPLAY_ORDER_INCREMENT;
129
115
  } else if (insertIndex === 0) {
130
- const firstOrder = visualOrder[0].display_order ?? visualOrder[0].id;
116
+ const firstOrder = visualOrder[0].display_order ?? visualOrder[0].id * DISPLAY_ORDER_INCREMENT;
131
117
  newOrder = firstOrder - DISPLAY_ORDER_INCREMENT;
132
118
  } else if (insertIndex >= visualOrder.length) {
133
- const lastOrder = visualOrder[visualOrder.length - 1].display_order ?? visualOrder[visualOrder.length - 1].id;
119
+ const lastOrder = visualOrder[visualOrder.length - 1].display_order ?? visualOrder[visualOrder.length - 1].id * DISPLAY_ORDER_INCREMENT;
134
120
  newOrder = lastOrder + DISPLAY_ORDER_INCREMENT;
135
121
  } else {
136
- const before = visualOrder[insertIndex - 1].display_order ?? visualOrder[insertIndex - 1].id;
137
- const after = visualOrder[insertIndex].display_order ?? visualOrder[insertIndex].id;
122
+ const before = visualOrder[insertIndex - 1].display_order ?? visualOrder[insertIndex - 1].id * DISPLAY_ORDER_INCREMENT;
123
+ const after = visualOrder[insertIndex].display_order ?? visualOrder[insertIndex].id * DISPLAY_ORDER_INCREMENT;
138
124
  newOrder = Math.floor((before + after) / 2);
139
125
  }
140
126
 
@@ -190,31 +176,62 @@ export function EpicGroup({ epicId, epicTitle, items, isInFlight = false, inFlig
190
176
  // Show reorder for ungrouped section when dragging an ungrouped card and cursor is over it
191
177
  const showUngroupedReorder = isOverUngroupedSection && isDragging && draggedItemEpicId === null;
192
178
 
193
- // Calculate insertion preview for this group - only for the active zone
179
+ // Insertion preview tracks pointer position via rAF loop so the line
180
+ // moves continuously even when DragContext change guards suppress re-renders.
194
181
  const showPreview = (showReorderHighlight || showRemoveFromEpicZone || showHighlight || showUngroupedReorder) && draggedItem;
195
- let insertAfterItemId: number | null | undefined = undefined; // undefined = no preview, null = at beginning
182
+ const [insertAfterItemId, setInsertAfterItemId] = useState<number | null | undefined>(undefined);
196
183
 
197
- if (showPreview && draggedItem) {
198
- const allPositions = getCardPositions();
199
- const itemIds = new Set(items.map(item => item.id));
200
- const groupPositions = allPositions
201
- .filter(pos => itemIds.has(pos.id) && pos.id !== draggedItem.id)
202
- .map(pos => ({
203
- id: pos.id,
204
- midY: (pos.rect.top + pos.rect.bottom) / 2,
205
- }))
206
- .sort((a, b) => a.midY - b.midY);
184
+ useEffect(() => {
185
+ if (!showPreview || !draggedItem) {
186
+ setInsertAfterItemId(undefined);
187
+ return;
188
+ }
207
189
 
208
- // Find which card the pointer is after
209
- insertAfterItemId = null; // Default to beginning
210
- for (const pos of groupPositions) {
211
- if (pointerY > pos.midY) {
212
- insertAfterItemId = pos.id;
213
- } else {
214
- break;
190
+ // Build item ID set once — items array is stable during a drag
191
+ const itemIds = new Set(items.map(item => item.id));
192
+ const dragId = draggedItem.id;
193
+
194
+ let rafId: number;
195
+ let lastTickTime = 0;
196
+ // Reusable array to avoid allocations per frame
197
+ const groupPositions: Array<{ id: number; midY: number }> = [];
198
+
199
+ const tick = () => {
200
+ const now = performance.now();
201
+ // Throttle to ~100ms — fast enough for smooth preview, avoids layout
202
+ // thrashing on slower hardware (Intel Macs).
203
+ if (now - lastTickTime < 100) {
204
+ rafId = requestAnimationFrame(tick);
205
+ return;
215
206
  }
216
- }
217
- }
207
+ lastTickTime = now;
208
+
209
+ const allPositions = getCardPositions();
210
+ groupPositions.length = 0;
211
+ for (const pos of allPositions) {
212
+ if (itemIds.has(pos.id) && pos.id !== dragId) {
213
+ groupPositions.push({ id: pos.id, midY: (pos.rect.top + pos.rect.bottom) / 2 });
214
+ }
215
+ }
216
+ groupPositions.sort((a, b) => a.midY - b.midY);
217
+
218
+ const currentPointerY = pointerPositionRef.current.y;
219
+ let newInsert: number | null = null;
220
+ for (const pos of groupPositions) {
221
+ if (currentPointerY > pos.midY) {
222
+ newInsert = pos.id;
223
+ } else {
224
+ break;
225
+ }
226
+ }
227
+
228
+ setInsertAfterItemId(prev => prev === newInsert ? prev : newInsert);
229
+ rafId = requestAnimationFrame(tick);
230
+ };
231
+
232
+ rafId = requestAnimationFrame(tick);
233
+ return () => cancelAnimationFrame(rafId);
234
+ }, [showPreview, draggedItem, items, getCardPositions, pointerPositionRef]);
218
235
 
219
236
  if (items.length === 0 && !showHighlight && !showReorderHighlight && !shouldRenderUngroupedZone) return null;
220
237
 
@@ -224,13 +241,13 @@ export function EpicGroup({ epicId, epicTitle, items, isInFlight = false, inFlig
224
241
  return (
225
242
  <div
226
243
  ref={setRefs}
227
- className={`${isStandaloneItem ? 'mb-2' : 'mb-6 p-3 -mx-3'} rounded-lg transition-[color,background-color,box-shadow] duration-200 ease-out ${
244
+ className={`${isStandaloneItem ? 'mb-2' : 'mb-6 p-3 -mx-3'} rounded-lg transition-[color,background-color] duration-200 ease-out ${
228
245
  showHighlight
229
- ? 'ring-2 ring-indigo-400 bg-indigo-100/50 dark:bg-indigo-900/30'
246
+ ? 'ring-2 ring-[#819D9F] bg-[#E8EEEF]/50 dark:bg-[#819D9F]/20'
230
247
  : showReorderHighlight
231
- ? 'ring-2 ring-purple-400 bg-purple-100/50 dark:bg-purple-900/30'
248
+ ? 'ring-2 ring-[#E3D985] bg-[#F9F7E8]/50 dark:bg-[#E3D985]/20'
232
249
  : showRemoveFromEpicZone
233
- ? 'ring-2 ring-orange-400 bg-orange-100/50 dark:bg-orange-900/30'
250
+ ? 'ring-2 ring-[#E57A44] bg-[#FCEEE6]/50 dark:bg-[#E57A44]/20'
234
251
  : ''
235
252
  }`}
236
253
  data-epic-id={epicId}
@@ -238,7 +255,8 @@ export function EpicGroup({ epicId, epicTitle, items, isInFlight = false, inFlig
238
255
  {epicTitle && (
239
256
  <div className="flex items-center gap-3 mb-3">
240
257
  <Link
241
- href={`/work/${epicId}`}
258
+ to={`/work/${epicId}`}
259
+ viewTransition
242
260
  className="group/epic flex items-center gap-1.5 text-base font-medium text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300"
243
261
  >
244
262
  <TypeIcon type="epic" />
@@ -263,12 +281,12 @@ export function EpicGroup({ epicId, epicTitle, items, isInFlight = false, inFlig
263
281
  </span>
264
282
  )}
265
283
  {showHighlight && (
266
- <span className="text-xs px-2 py-1 rounded bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:text-indigo-300">
284
+ <span className="text-xs px-2 py-1 rounded bg-[#E8EEEF] text-[#4A6365] dark:bg-[#819D9F]/20 dark:text-[#819D9F]">
267
285
  drop to assign
268
286
  </span>
269
287
  )}
270
288
  {showReorderHighlight && (
271
- <span className="text-xs px-2 py-1 rounded bg-purple-100 text-purple-700 dark:bg-purple-900/50 dark:text-purple-300">
289
+ <span className="text-xs px-2 py-1 rounded bg-[#F9F7E8] text-[#8B7D2F] dark:bg-[#E3D985]/20 dark:text-[#E3D985]">
272
290
  reorder
273
291
  </span>
274
292
  )}
@@ -277,27 +295,33 @@ export function EpicGroup({ epicId, epicTitle, items, isInFlight = false, inFlig
277
295
  {/* Ungrouped section header - shown when dragging from epic */}
278
296
  {isUngroupedSection && showRemoveFromEpicZone && items.length === 0 && (
279
297
  <div className="flex items-center gap-3 py-4">
280
- <span className="text-base font-medium text-orange-600 dark:text-orange-400">
298
+ <span className="text-base font-medium text-[#E57A44] dark:text-[#E57A44]">
281
299
  Drop here to remove from epic
282
300
  </span>
283
301
  </div>
284
302
  )}
285
303
  {isUngroupedSection && items.length > 0 && isDraggable && showRemoveFromEpicZone && (
286
304
  <div className="flex items-center gap-3 mb-3">
287
- <span className="text-xs px-2 py-1 rounded bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300">
305
+ <span className="text-xs px-2 py-1 rounded bg-[#FCEEE6] text-[#9E4A1E] dark:bg-[#E57A44]/20 dark:text-[#E57A44]">
288
306
  drop to remove from epic
289
307
  </span>
290
308
  </div>
291
309
  )}
292
310
  <div className="space-y-3">
293
- {/* Placeholder at the beginning (insertAfterItemId === null) */}
294
- <AnimatePresence>
295
- {insertAfterItemId === null && (
296
- <PlaceholderCard key="placeholder-start" />
297
- )}
298
- </AnimatePresence>
299
- {items.map((item) => (
311
+ {items.map((item, index) => (
300
312
  <div key={item.id}>
313
+ {/* CSS insertion indicator — replaces AnimatePresence+PlaceholderCard for less overhead */}
314
+ {index === 0 && insertAfterItemId === null && (
315
+ <div
316
+ data-testid="drag-placeholder"
317
+ style={{
318
+ height: 3,
319
+ borderRadius: 2,
320
+ background: 'linear-gradient(90deg, transparent, #819D9F, transparent)',
321
+ margin: '2px 8px 8px',
322
+ }}
323
+ />
324
+ )}
301
325
  <DraggableCard item={item} disabled={!isDraggable}>
302
326
  <KanbanCard
303
327
  item={item}
@@ -306,7 +330,7 @@ export function EpicGroup({ epicId, epicTitle, items, isInFlight = false, inFlig
306
330
  onReject={onReject}
307
331
  onRestart={onRestart}
308
332
  onTriggerClaude={onTriggerClaude}
309
- hasActiveSession={activeSessions?.has(String(item.id))}
333
+ hasActiveSession={activeSessionIds?.has(String(item.id))}
310
334
  onOpenSession={onOpenSession}
311
335
  onCloseSession={onCloseSession}
312
336
  usageAllowed={usageAllowed}
@@ -315,15 +339,21 @@ export function EpicGroup({ epicId, epicTitle, items, isInFlight = false, inFlig
315
339
  isHighlighted={false}
316
340
  />
317
341
  </DraggableCard>
318
- {/* Placeholder after this card */}
319
- <AnimatePresence>
320
- {insertAfterItemId === item.id && (
321
- <PlaceholderCard key={`placeholder-${item.id}`} />
322
- )}
323
- </AnimatePresence>
342
+ {/* CSS insertion indicator after this card */}
343
+ {insertAfterItemId === item.id && (
344
+ <div
345
+ data-testid="drag-placeholder"
346
+ style={{
347
+ height: 3,
348
+ borderRadius: 2,
349
+ background: 'linear-gradient(90deg, transparent, #819D9F, transparent)',
350
+ margin: '8px 8px 2px',
351
+ }}
352
+ />
353
+ )}
324
354
  </div>
325
355
  ))}
326
356
  </div>
327
357
  </div>
328
358
  );
329
- }
359
+ });
@@ -1,4 +1,3 @@
1
- 'use client';
2
1
 
3
2
  import { useState } from 'react';
4
3
  import type { ClaudeMessage } from '../lib/session-stream-manager';
@@ -1,4 +1,3 @@
1
- 'use client';
2
1
 
3
2
  import { useState } from 'react';
4
3
 
@@ -58,7 +57,7 @@ export function GateChoiceCard({
58
57
  onMouseLeave={() => setHoveredId(null)}
59
58
  disabled={disabled && !isSelected}
60
59
  className={`
61
- w-full text-left rounded-xl border-2 p-4 transition-[color,background-color,border-color,box-shadow,transform] duration-200 ease-out
60
+ w-full text-left rounded-xl border-2 p-4 transition-[color,background-color,border-color] duration-200 ease-out
62
61
  ${isSelected
63
62
  ? 'border-[#819D9F] dark:border-[#819D9F] bg-[#e8f0f0] dark:bg-[#819D9F]/20 ring-2 ring-[#819D9F]/30'
64
63
  : 'border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-600'
@@ -1,7 +1,4 @@
1
- 'use client';
2
-
3
1
  import { useState, useEffect } from 'react';
4
- import Image from 'next/image';
5
2
  import { Button } from '@/components/ui/Button';
6
3
 
7
4
  const statusMessages = [
@@ -73,12 +70,11 @@ export function InstallClaudeScreen({
73
70
  <div className="max-w-md w-full space-y-10">
74
71
  {/* Logo */}
75
72
  <div className="flex flex-col items-center space-y-6">
76
- <Image
73
+ <img
77
74
  src="/jettypod_wordmark.png"
78
75
  alt="JettyPod"
79
76
  width={160}
80
77
  height={40}
81
- priority
82
78
  />
83
79
  {!isInstalling && !isSuccess && (
84
80
  <>
@@ -1,4 +1,3 @@
1
- 'use client';
2
1
 
3
2
  import { useState, useEffect } from 'react';
4
3