jettypod 4.4.115 → 4.4.118
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 +7 -0
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +25 -9
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +7 -3
- package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
- package/apps/dashboard/app/api/usage/route.ts +17 -0
- package/apps/dashboard/app/connect-claude/page.tsx +24 -0
- package/apps/dashboard/app/install-claude/page.tsx +8 -6
- package/apps/dashboard/app/login/page.tsx +229 -0
- package/apps/dashboard/app/page.tsx +5 -3
- package/apps/dashboard/app/settings/page.tsx +2 -0
- package/apps/dashboard/app/subscribe/page.tsx +11 -0
- package/apps/dashboard/app/welcome/page.tsx +23 -0
- package/apps/dashboard/components/AppShell.tsx +51 -9
- package/apps/dashboard/components/CardMenu.tsx +14 -5
- package/apps/dashboard/components/ClaudePanel.tsx +65 -9
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +223 -0
- package/apps/dashboard/components/DragContext.tsx +73 -64
- package/apps/dashboard/components/DraggableCard.tsx +6 -46
- package/apps/dashboard/components/GateCard.tsx +21 -0
- package/apps/dashboard/components/InstallClaudeScreen.tsx +132 -30
- package/apps/dashboard/components/KanbanBoard.tsx +173 -56
- package/apps/dashboard/components/PlaceholderCard.tsx +9 -19
- package/apps/dashboard/components/ProjectSwitcher.tsx +28 -0
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +34 -3
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +30 -2
- package/apps/dashboard/components/SubscribeContent.tsx +191 -0
- package/apps/dashboard/components/TipCard.tsx +176 -0
- package/apps/dashboard/components/UpgradeBanner.tsx +29 -0
- package/apps/dashboard/components/WelcomeScreen.tsx +14 -4
- package/apps/dashboard/components/settings/AccountSection.tsx +163 -0
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +292 -29
- package/apps/dashboard/contexts/UsageContext.tsx +131 -0
- package/apps/dashboard/contexts/usageHelpers.js +9 -0
- package/apps/dashboard/electron/ipc-handlers.js +220 -114
- package/apps/dashboard/electron/main.js +415 -37
- package/apps/dashboard/electron/preload.js +23 -4
- package/apps/dashboard/electron/session-manager.js +141 -0
- package/apps/dashboard/electron-builder.config.js +3 -5
- package/apps/dashboard/lib/claude-process-manager.ts +6 -4
- package/apps/dashboard/lib/db-bridge.ts +32 -0
- package/apps/dashboard/lib/db.ts +159 -13
- package/apps/dashboard/lib/session-state-machine.ts +3 -0
- package/apps/dashboard/lib/session-stream-manager.ts +76 -13
- package/apps/dashboard/lib/tests.ts +3 -1
- package/apps/dashboard/next.config.js +19 -14
- package/apps/dashboard/package.json +3 -1
- package/apps/dashboard/scripts/upload-to-r2.js +89 -0
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
- package/apps/update-server/package.json +16 -0
- package/apps/update-server/schema.sql +31 -0
- package/apps/update-server/src/index.ts +1074 -0
- package/apps/update-server/tsconfig.json +16 -0
- package/apps/update-server/wrangler.toml +35 -0
- package/docs/bdd-guidance.md +390 -0
- package/jettypod.js +5 -4
- package/lib/migrations/027-plan-at-creation-column.js +31 -0
- package/lib/migrations/028-ready-for-review-column.js +27 -0
- package/lib/schema.js +3 -1
- package/lib/seed-onboarding.js +100 -68
- package/lib/work-commands/index.js +43 -13
- package/lib/work-tracking/index.js +46 -27
- package/package.json +1 -1
- package/skills-templates/bug-mode/SKILL.md +5 -11
- package/skills-templates/request-routing/SKILL.md +24 -11
- package/skills-templates/simple-improvement/SKILL.md +35 -19
- package/skills-templates/stable-mode/SKILL.md +5 -6
- package/templates/bdd-guidance.md +139 -0
- package/templates/bdd-scaffolding/wait.js +18 -0
- package/templates/bdd-scaffolding/world.js +19 -0
- package/.jettypod-backup/work.db +0 -0
- package/apps/dashboard/app/access-code/page.tsx +0 -110
- package/lib/discovery-checkpoint.js +0 -123
- package/skills-templates/project-discovery/SKILL.md +0 -372
|
@@ -47,15 +47,11 @@ interface DragContextType {
|
|
|
47
47
|
draggedItem: WorkItem | null;
|
|
48
48
|
activeDropZone: string | null;
|
|
49
49
|
activeEpicZone: string | null;
|
|
50
|
-
dragPosition: { x: number; y: number };
|
|
51
|
-
draggedCardHeight: number;
|
|
52
50
|
setDraggedItem: (item: WorkItem | null) => void;
|
|
53
51
|
registerDropZone: (id: string, info: DropZoneInfo) => void;
|
|
54
52
|
unregisterDropZone: (id: string) => void;
|
|
55
53
|
registerEpicDropZone: (id: string, info: EpicDropZoneInfo) => void;
|
|
56
54
|
unregisterEpicDropZone: (id: string) => void;
|
|
57
|
-
registerCardPosition: (id: number, rect: DOMRect) => void;
|
|
58
|
-
unregisterCard: (id: number) => void;
|
|
59
55
|
getCardPositions: () => CardPosition[];
|
|
60
56
|
}
|
|
61
57
|
|
|
@@ -64,15 +60,11 @@ const DragContext = createContext<DragContextType>({
|
|
|
64
60
|
draggedItem: null,
|
|
65
61
|
activeDropZone: null,
|
|
66
62
|
activeEpicZone: null,
|
|
67
|
-
dragPosition: { x: 0, y: 0 },
|
|
68
|
-
draggedCardHeight: 0,
|
|
69
63
|
setDraggedItem: () => {},
|
|
70
64
|
registerDropZone: () => {},
|
|
71
65
|
unregisterDropZone: () => {},
|
|
72
66
|
registerEpicDropZone: () => {},
|
|
73
67
|
unregisterEpicDropZone: () => {},
|
|
74
|
-
registerCardPosition: () => {},
|
|
75
|
-
unregisterCard: () => {},
|
|
76
68
|
getCardPositions: () => [],
|
|
77
69
|
});
|
|
78
70
|
|
|
@@ -118,12 +110,8 @@ export function DragProvider({ children, renderDragOverlay, onRemoveFromEpic, on
|
|
|
118
110
|
const [draggedItem, setDraggedItem] = useState<WorkItem | null>(null);
|
|
119
111
|
const [activeDropZone, setActiveDropZone] = useState<string | null>(null);
|
|
120
112
|
const [activeEpicZone, setActiveEpicZone] = useState<string | null>(null);
|
|
121
|
-
const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 });
|
|
122
|
-
const [draggedCardHeight, setDraggedCardHeight] = useState(0);
|
|
123
|
-
|
|
124
113
|
const dropZonesRef = useRef<Map<string, DropZoneInfo>>(new Map());
|
|
125
114
|
const epicDropZonesRef = useRef<Map<string, EpicDropZoneInfo>>(new Map());
|
|
126
|
-
const cardPositionsRef = useRef<Map<number, DOMRect>>(new Map());
|
|
127
115
|
const draggedItemRef = useRef<WorkItem | null>(null);
|
|
128
116
|
const pointerPositionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
129
117
|
|
|
@@ -159,22 +147,16 @@ export function DragProvider({ children, renderDragOverlay, onRemoveFromEpic, on
|
|
|
159
147
|
epicDropZonesRef.current.delete(id);
|
|
160
148
|
}, []);
|
|
161
149
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}, []);
|
|
165
|
-
|
|
166
|
-
const unregisterCard = useCallback((id: number) => {
|
|
167
|
-
// Don't unregister if this is the dragged card
|
|
168
|
-
if (draggedItemRef.current?.id === id) {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
cardPositionsRef.current.delete(id);
|
|
172
|
-
}, []);
|
|
173
|
-
|
|
150
|
+
// Read fresh card positions from DOM - avoids stale cached positions
|
|
151
|
+
// that cause jank when cards shift during drag (collapsed dragged card, placeholders)
|
|
174
152
|
const getCardPositions = useCallback((): CardPosition[] => {
|
|
175
153
|
const positions: CardPosition[] = [];
|
|
176
|
-
|
|
177
|
-
|
|
154
|
+
const elements = document.querySelectorAll<HTMLElement>('[data-item-id]');
|
|
155
|
+
elements.forEach((el) => {
|
|
156
|
+
const id = Number(el.getAttribute('data-item-id'));
|
|
157
|
+
if (!isNaN(id) && el.offsetHeight > 0) {
|
|
158
|
+
positions.push({ id, rect: el.getBoundingClientRect() });
|
|
159
|
+
}
|
|
178
160
|
});
|
|
179
161
|
return positions;
|
|
180
162
|
}, []);
|
|
@@ -189,40 +171,34 @@ export function DragProvider({ children, renderDragOverlay, onRemoveFromEpic, on
|
|
|
189
171
|
if (item) {
|
|
190
172
|
setDraggedItem(item);
|
|
191
173
|
draggedItemRef.current = item;
|
|
192
|
-
// Get the card height from the dragging element
|
|
193
|
-
const rect = event.active.rect.current.initial;
|
|
194
|
-
if (rect) {
|
|
195
|
-
setDraggedCardHeight(rect.height);
|
|
196
|
-
}
|
|
197
174
|
}
|
|
198
175
|
}, []);
|
|
199
176
|
|
|
200
177
|
const handleDragMove = useCallback((event: { activatorEvent: Event; delta: { x: number; y: number } }) => {
|
|
201
|
-
//
|
|
178
|
+
// Update ref for reorder calculations (state update handled by native pointermove)
|
|
202
179
|
const pointerEvent = event.activatorEvent as PointerEvent;
|
|
203
180
|
if (pointerEvent) {
|
|
204
181
|
const x = pointerEvent.clientX + event.delta.x;
|
|
205
182
|
const y = pointerEvent.clientY + event.delta.y;
|
|
206
183
|
pointerPositionRef.current = { x, y };
|
|
207
|
-
setDragPosition({ x, y });
|
|
208
184
|
}
|
|
209
185
|
}, []);
|
|
210
186
|
|
|
211
187
|
const handleDragOver = useCallback((event: DragOverEvent) => {
|
|
212
188
|
const { over, activatorEvent, delta } = event;
|
|
213
189
|
|
|
214
|
-
// Update pointer
|
|
190
|
+
// Update pointer ref (state update handled by native pointermove)
|
|
215
191
|
const pointerEvent = activatorEvent as PointerEvent;
|
|
216
192
|
if (pointerEvent) {
|
|
217
193
|
const x = pointerEvent.clientX + delta.x;
|
|
218
194
|
const y = pointerEvent.clientY + delta.y;
|
|
219
195
|
pointerPositionRef.current = { x, y };
|
|
220
|
-
setDragPosition({ x, y });
|
|
221
196
|
}
|
|
222
197
|
|
|
223
198
|
if (!over) {
|
|
224
|
-
|
|
225
|
-
|
|
199
|
+
// Don't clear zone state on null - collision detection has gaps during drag
|
|
200
|
+
// that cause the insertion preview to flicker/disappear. Zones are properly
|
|
201
|
+
// cleared when drag ends or is cancelled.
|
|
226
202
|
return;
|
|
227
203
|
}
|
|
228
204
|
|
|
@@ -244,7 +220,19 @@ export function DragProvider({ children, renderDragOverlay, onRemoveFromEpic, on
|
|
|
244
220
|
setActiveDropZone(foundStatusZone);
|
|
245
221
|
} else if (dropZonesRef.current.has(overId)) {
|
|
246
222
|
setActiveDropZone(overId);
|
|
247
|
-
|
|
223
|
+
// Epic zones are nested inside status zones, so collision detection
|
|
224
|
+
// frequently returns the status zone instead. Check if pointer is
|
|
225
|
+
// also within an epic zone (mirror of status zone check above).
|
|
226
|
+
let foundEpicZone: string | null = null;
|
|
227
|
+
epicDropZonesRef.current.forEach((info, id) => {
|
|
228
|
+
const rect = info.element.getBoundingClientRect();
|
|
229
|
+
const x = pointerPositionRef.current.x;
|
|
230
|
+
const y = pointerPositionRef.current.y;
|
|
231
|
+
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
|
|
232
|
+
foundEpicZone = id;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
setActiveEpicZone(foundEpicZone);
|
|
248
236
|
} else {
|
|
249
237
|
setActiveDropZone(null);
|
|
250
238
|
setActiveEpicZone(null);
|
|
@@ -254,11 +242,13 @@ export function DragProvider({ children, renderDragOverlay, onRemoveFromEpic, on
|
|
|
254
242
|
const handleDragEnd = useCallback(async (event: DragEndEvent) => {
|
|
255
243
|
const { over, activatorEvent, delta } = event;
|
|
256
244
|
|
|
257
|
-
// Get final pointer position
|
|
245
|
+
// Get final pointer position (both x and y needed for epic zone bounds check)
|
|
258
246
|
const pointerEvent = activatorEvent as PointerEvent;
|
|
259
247
|
if (pointerEvent) {
|
|
260
|
-
|
|
261
|
-
|
|
248
|
+
pointerPositionRef.current = {
|
|
249
|
+
x: pointerEvent.clientX + delta.x,
|
|
250
|
+
y: pointerEvent.clientY + delta.y,
|
|
251
|
+
};
|
|
262
252
|
}
|
|
263
253
|
|
|
264
254
|
const item = draggedItemRef.current;
|
|
@@ -291,27 +281,43 @@ export function DragProvider({ children, renderDragOverlay, onRemoveFromEpic, on
|
|
|
291
281
|
}
|
|
292
282
|
}
|
|
293
283
|
} else if (overId) {
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
284
|
+
// Collision detection returned a status zone, but epic zones are nested
|
|
285
|
+
// inside status zones. Check if pointer is actually within an epic zone.
|
|
286
|
+
let resolvedEpicZone: string | null = null;
|
|
287
|
+
epicDropZonesRef.current.forEach((info, id) => {
|
|
288
|
+
const rect = info.element.getBoundingClientRect();
|
|
289
|
+
const x = pointerPositionRef.current.x;
|
|
290
|
+
const y = pointerPositionRef.current.y;
|
|
291
|
+
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
|
|
292
|
+
resolvedEpicZone = id;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (resolvedEpicZone) {
|
|
297
|
+
// Pointer is within an epic zone - route to epic handler
|
|
298
|
+
const epicZoneInfo = epicDropZonesRef.current.get(resolvedEpicZone);
|
|
299
|
+
if (epicZoneInfo) {
|
|
300
|
+
if (currentEpicId === epicZoneInfo.epicId && epicZoneInfo.onReorder) {
|
|
301
|
+
await epicZoneInfo.onReorder(item.id, pointerPositionRef.current.y);
|
|
302
|
+
} else if (currentEpicId !== epicZoneInfo.epicId) {
|
|
303
|
+
await epicZoneInfo.onEpicAssign(item.id, epicZoneInfo.epicId);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
// Truly a status zone drop
|
|
308
|
+
const zoneInfo = dropZonesRef.current.get(overId);
|
|
309
|
+
if (zoneInfo) {
|
|
310
|
+
if (item.status !== zoneInfo.targetStatus) {
|
|
311
|
+
await zoneInfo.onDrop(item.id, zoneInfo.targetStatus);
|
|
312
|
+
} else if (zoneInfo.onReorder) {
|
|
313
|
+
await zoneInfo.onReorder(item.id, pointerPositionRef.current.y);
|
|
314
|
+
}
|
|
304
315
|
}
|
|
305
|
-
} else if (currentEpicId && onRemoveFromEpic) {
|
|
306
|
-
// Dropped on unknown zone - remove from epic if applicable
|
|
307
|
-
await onRemoveFromEpic(item.id, null);
|
|
308
|
-
}
|
|
309
|
-
} else {
|
|
310
|
-
// Dropped outside all zones
|
|
311
|
-
if (currentEpicId && onRemoveFromEpic) {
|
|
312
|
-
await onRemoveFromEpic(item.id, null);
|
|
313
316
|
}
|
|
314
317
|
}
|
|
318
|
+
// over: null means collision detection missed - treat as no-op.
|
|
319
|
+
// Same gap issue we handle in handleDragOver. Don't remove from epic
|
|
320
|
+
// just because the collision detection had a gap at the moment of drop.
|
|
315
321
|
} catch (error) {
|
|
316
322
|
const errorMessage = error instanceof Error ? error.message : 'Failed to complete drop operation';
|
|
317
323
|
onError?.(errorMessage);
|
|
@@ -351,15 +357,11 @@ export function DragProvider({ children, renderDragOverlay, onRemoveFromEpic, on
|
|
|
351
357
|
draggedItem,
|
|
352
358
|
activeDropZone,
|
|
353
359
|
activeEpicZone,
|
|
354
|
-
dragPosition,
|
|
355
|
-
draggedCardHeight,
|
|
356
360
|
setDraggedItem: handleSetDraggedItem,
|
|
357
361
|
registerDropZone,
|
|
358
362
|
unregisterDropZone,
|
|
359
363
|
registerEpicDropZone,
|
|
360
364
|
unregisterEpicDropZone,
|
|
361
|
-
registerCardPosition,
|
|
362
|
-
unregisterCard,
|
|
363
365
|
getCardPositions,
|
|
364
366
|
}}
|
|
365
367
|
>
|
|
@@ -374,7 +376,14 @@ export function DragProvider({ children, renderDragOverlay, onRemoveFromEpic, on
|
|
|
374
376
|
>
|
|
375
377
|
{children}
|
|
376
378
|
{/* Drag overlay - uses @dnd-kit's built-in DragOverlay */}
|
|
377
|
-
<DragOverlay dropAnimation={
|
|
379
|
+
<DragOverlay dropAnimation={{
|
|
380
|
+
duration: 150,
|
|
381
|
+
easing: 'ease',
|
|
382
|
+
keyframes: ({ transform }) => [
|
|
383
|
+
{ opacity: 1, transform: transform.initial ? `translate3d(${transform.initial.x}px, ${transform.initial.y}px, 0)` : undefined },
|
|
384
|
+
{ opacity: 0, transform: transform.initial ? `translate3d(${transform.initial.x}px, ${transform.initial.y}px, 0)` : undefined },
|
|
385
|
+
],
|
|
386
|
+
}}>
|
|
378
387
|
{draggedItem && renderDragOverlay ? (
|
|
379
388
|
<div
|
|
380
389
|
style={{
|
|
@@ -12,8 +12,7 @@ interface DraggableCardProps {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function DraggableCard({ item, children, disabled = false }: DraggableCardProps) {
|
|
15
|
-
const {
|
|
16
|
-
const cardRef = useRef<HTMLDivElement>(null);
|
|
15
|
+
const { draggedItem, setDraggedItem } = useDragContext();
|
|
17
16
|
const prevDisabledRef = useRef(disabled);
|
|
18
17
|
|
|
19
18
|
const {
|
|
@@ -33,65 +32,26 @@ export function DraggableCard({ item, children, disabled = false }: DraggableCar
|
|
|
33
32
|
const isNowDisabled = disabled;
|
|
34
33
|
prevDisabledRef.current = disabled;
|
|
35
34
|
|
|
36
|
-
// If this card was being dragged and just became disabled, cancel the drag
|
|
37
35
|
if (wasEnabled && isNowDisabled && draggedItem?.id === item.id) {
|
|
38
36
|
setDraggedItem(null);
|
|
39
37
|
}
|
|
40
38
|
}, [disabled, draggedItem, item.id, setDraggedItem]);
|
|
41
39
|
|
|
42
|
-
// Register card position for optimized reorder calculations
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (disabled || !cardRef.current) return;
|
|
45
|
-
|
|
46
|
-
const updatePosition = () => {
|
|
47
|
-
if (cardRef.current) {
|
|
48
|
-
registerCardPosition(item.id, cardRef.current.getBoundingClientRect());
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// Initial registration
|
|
53
|
-
updatePosition();
|
|
54
|
-
|
|
55
|
-
// Update on resize/layout changes
|
|
56
|
-
const resizeObserver = new ResizeObserver(updatePosition);
|
|
57
|
-
resizeObserver.observe(cardRef.current);
|
|
58
|
-
|
|
59
|
-
// Update on scroll (positions are viewport-relative)
|
|
60
|
-
const scrollHandler = () => updatePosition();
|
|
61
|
-
window.addEventListener('scroll', scrollHandler, true);
|
|
62
|
-
|
|
63
|
-
return () => {
|
|
64
|
-
unregisterCard(item.id);
|
|
65
|
-
resizeObserver.disconnect();
|
|
66
|
-
window.removeEventListener('scroll', scrollHandler, true);
|
|
67
|
-
};
|
|
68
|
-
}, [item.id, disabled, registerCardPosition, unregisterCard]);
|
|
69
|
-
|
|
70
|
-
// Combine refs
|
|
71
|
-
const setRefs = (node: HTMLDivElement | null) => {
|
|
72
|
-
setNodeRef(node);
|
|
73
|
-
(cardRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
40
|
if (disabled) {
|
|
77
41
|
return <>{children}</>;
|
|
78
42
|
}
|
|
79
43
|
|
|
80
|
-
// When dragging,
|
|
44
|
+
// When dragging, fade the original card but keep its space to prevent layout jumping.
|
|
45
|
+
// The DragOverlay shows the card at the cursor; the thin insertion line shows where it'll land.
|
|
81
46
|
const style = {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
opacity: isDragging ? 0 : 1,
|
|
85
|
-
height: isDragging ? 0 : 'auto',
|
|
86
|
-
overflow: isDragging ? 'hidden' : 'visible',
|
|
87
|
-
padding: isDragging ? 0 : undefined,
|
|
88
|
-
marginBottom: isDragging ? 0 : undefined,
|
|
47
|
+
opacity: isDragging ? 0.2 : 1,
|
|
48
|
+
transition: 'opacity 150ms ease',
|
|
89
49
|
touchAction: 'none' as const,
|
|
90
50
|
};
|
|
91
51
|
|
|
92
52
|
return (
|
|
93
53
|
<div
|
|
94
|
-
ref={
|
|
54
|
+
ref={setNodeRef}
|
|
95
55
|
style={style}
|
|
96
56
|
className="cursor-grab active:cursor-grabbing"
|
|
97
57
|
data-draggable="true"
|
|
@@ -5,6 +5,7 @@ import type { ClaudeMessage } from '../lib/session-stream-manager';
|
|
|
5
5
|
import { GateChoiceCard } from './GateChoiceCard';
|
|
6
6
|
import type { ChoiceOption } from './GateChoiceCard';
|
|
7
7
|
import { ModeStartCard, isModeStartGate } from './ModeStartCard';
|
|
8
|
+
import { TipCard } from './TipCard';
|
|
8
9
|
|
|
9
10
|
// Gate display configuration matching JettyPod design system
|
|
10
11
|
const GATE_CONFIG: Record<string, {
|
|
@@ -117,6 +118,17 @@ const GATE_CONFIG: Record<string, {
|
|
|
117
118
|
darkText: 'dark:text-zinc-300',
|
|
118
119
|
fileMono: 'text-indigo-600',
|
|
119
120
|
},
|
|
121
|
+
'tip': {
|
|
122
|
+
emoji: '💡',
|
|
123
|
+
label: 'Tip',
|
|
124
|
+
bg: 'bg-teal-50',
|
|
125
|
+
darkBg: 'dark:bg-teal-900/20',
|
|
126
|
+
border: 'border-teal-200',
|
|
127
|
+
darkBorder: 'dark:border-teal-800',
|
|
128
|
+
text: 'text-zinc-700',
|
|
129
|
+
darkText: 'dark:text-zinc-300',
|
|
130
|
+
fileMono: 'text-teal-600',
|
|
131
|
+
},
|
|
120
132
|
};
|
|
121
133
|
|
|
122
134
|
const DEFAULT_CONFIG = {
|
|
@@ -207,6 +219,15 @@ export function GateCard({ message, isLatest = false, onAnswerQuestion, answered
|
|
|
207
219
|
return <ModeStartCard gateType={gateType} />;
|
|
208
220
|
}
|
|
209
221
|
|
|
222
|
+
// Tip gates render as dismissible guidance cards
|
|
223
|
+
if (gateType === 'tip') {
|
|
224
|
+
const tipId = (gateData.id as string) || `tip-${message.timestamp}`;
|
|
225
|
+
const icon = (gateData.icon as string) || '💡';
|
|
226
|
+
const title = (gateData.title as string) || 'Tip';
|
|
227
|
+
const body = (gateData.body as string) || '';
|
|
228
|
+
return <TipCard tipId={tipId} icon={icon} title={title} body={body} />;
|
|
229
|
+
}
|
|
230
|
+
|
|
210
231
|
// Question gates render as interactive choice cards
|
|
211
232
|
if (gateType === 'question') {
|
|
212
233
|
const question = (gateData.question as string) || 'A decision is needed';
|
|
@@ -1,20 +1,74 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
3
4
|
import Image from 'next/image';
|
|
4
5
|
|
|
6
|
+
const statusMessages = [
|
|
7
|
+
"Downloading Claude Code...",
|
|
8
|
+
"Setting Up Environment...",
|
|
9
|
+
"Configuring Permissions...",
|
|
10
|
+
"Almost There...",
|
|
11
|
+
];
|
|
12
|
+
|
|
5
13
|
interface InstallClaudeScreenProps {
|
|
6
14
|
onInstall: () => void;
|
|
7
15
|
isInstalling: boolean;
|
|
8
|
-
|
|
16
|
+
isSuccess: boolean;
|
|
9
17
|
}
|
|
10
18
|
|
|
11
19
|
export function InstallClaudeScreen({
|
|
12
20
|
onInstall,
|
|
13
21
|
isInstalling,
|
|
14
|
-
|
|
22
|
+
isSuccess,
|
|
15
23
|
}: InstallClaudeScreenProps) {
|
|
24
|
+
const [messageIndex, setMessageIndex] = useState(0);
|
|
25
|
+
const [progress, setProgress] = useState(0);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!isInstalling || isSuccess) return;
|
|
29
|
+
const interval = setInterval(() => {
|
|
30
|
+
setMessageIndex((prev) => (prev + 1) % statusMessages.length);
|
|
31
|
+
}, 2500);
|
|
32
|
+
return () => clearInterval(interval);
|
|
33
|
+
}, [isInstalling, isSuccess]);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!isInstalling || isSuccess) return;
|
|
37
|
+
const interval = setInterval(() => {
|
|
38
|
+
setProgress((prev) => Math.min(prev + 2, 90));
|
|
39
|
+
}, 300);
|
|
40
|
+
return () => clearInterval(interval);
|
|
41
|
+
}, [isInstalling, isSuccess]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (isSuccess) setProgress(100);
|
|
45
|
+
}, [isSuccess]);
|
|
46
|
+
|
|
16
47
|
return (
|
|
17
48
|
<div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900 p-8">
|
|
49
|
+
<style jsx>{`
|
|
50
|
+
@keyframes pulse {
|
|
51
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
52
|
+
50% { opacity: 0.5; transform: scale(1.1); }
|
|
53
|
+
}
|
|
54
|
+
@keyframes drawCheck {
|
|
55
|
+
to { stroke-dashoffset: 0; }
|
|
56
|
+
}
|
|
57
|
+
@keyframes fadeIn {
|
|
58
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
59
|
+
to { opacity: 1; transform: translateY(0); }
|
|
60
|
+
}
|
|
61
|
+
.pulse-dot {
|
|
62
|
+
animation: pulse 1.5s ease-in-out infinite;
|
|
63
|
+
}
|
|
64
|
+
.checkmark-draw {
|
|
65
|
+
animation: drawCheck 0.6s ease-out forwards;
|
|
66
|
+
}
|
|
67
|
+
.fade-in {
|
|
68
|
+
animation: fadeIn 0.4s ease-out forwards;
|
|
69
|
+
}
|
|
70
|
+
`}</style>
|
|
71
|
+
|
|
18
72
|
<div className="max-w-md w-full space-y-8">
|
|
19
73
|
{/* Logo */}
|
|
20
74
|
<div className="flex flex-col items-center space-y-4">
|
|
@@ -25,27 +79,73 @@ export function InstallClaudeScreen({
|
|
|
25
79
|
height={40}
|
|
26
80
|
priority
|
|
27
81
|
/>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
82
|
+
{!isInstalling && !isSuccess && (
|
|
83
|
+
<>
|
|
84
|
+
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
|
|
85
|
+
Claude Code Required
|
|
86
|
+
</h1>
|
|
87
|
+
<p className="text-zinc-500 dark:text-zinc-400 text-center">
|
|
88
|
+
JettyPod requires Claude Code to be installed.
|
|
89
|
+
Click below to install it automatically.
|
|
90
|
+
</p>
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
35
93
|
</div>
|
|
36
94
|
|
|
37
|
-
{/* Install Button
|
|
95
|
+
{/* Install Button / Progress / Success */}
|
|
38
96
|
<div className="pt-4">
|
|
39
|
-
{
|
|
40
|
-
<div className="space-y-
|
|
41
|
-
|
|
42
|
-
|
|
97
|
+
{isSuccess ? (
|
|
98
|
+
<div className="flex flex-col items-center space-y-6 fade-in" data-testid="install-success">
|
|
99
|
+
{/* Animated Checkmark */}
|
|
100
|
+
<div
|
|
101
|
+
className="w-20 h-20 rounded-full flex items-center justify-center"
|
|
102
|
+
style={{ backgroundColor: '#819D9F' }}
|
|
103
|
+
>
|
|
104
|
+
<svg width="40" height="40" viewBox="0 0 40 40" fill="none">
|
|
105
|
+
<polyline
|
|
106
|
+
points="10,20 18,28 30,12"
|
|
107
|
+
stroke="white"
|
|
108
|
+
strokeWidth="3"
|
|
109
|
+
strokeLinecap="round"
|
|
110
|
+
strokeLinejoin="round"
|
|
111
|
+
fill="none"
|
|
112
|
+
strokeDasharray="40"
|
|
113
|
+
strokeDashoffset="40"
|
|
114
|
+
className="checkmark-draw"
|
|
115
|
+
/>
|
|
116
|
+
</svg>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<h2 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
|
|
120
|
+
Claude Code Installed
|
|
121
|
+
</h2>
|
|
122
|
+
<p className="text-zinc-500 dark:text-zinc-400 text-center">
|
|
123
|
+
Redirecting you to connect your account...
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
) : isInstalling ? (
|
|
127
|
+
<div className="space-y-6" data-testid="install-progress">
|
|
128
|
+
{/* Pulse dot + status message */}
|
|
129
|
+
<div className="flex items-center justify-center space-x-3">
|
|
130
|
+
<div
|
|
131
|
+
className="w-3 h-3 rounded-full pulse-dot"
|
|
132
|
+
style={{ backgroundColor: '#819D9F' }}
|
|
133
|
+
/>
|
|
134
|
+
<span className="text-zinc-600 dark:text-zinc-300 font-medium">
|
|
135
|
+
{statusMessages[messageIndex]}
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Progress bar */}
|
|
140
|
+
<div className="w-full h-1 bg-zinc-200 dark:bg-zinc-700 rounded-full overflow-hidden">
|
|
141
|
+
<div
|
|
142
|
+
className="h-full rounded-full transition-all duration-300 ease-out"
|
|
143
|
+
style={{
|
|
144
|
+
width: `${progress}%`,
|
|
145
|
+
background: 'linear-gradient(90deg, #c8d9da, #819D9F)',
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
43
148
|
</div>
|
|
44
|
-
{installProgress && (
|
|
45
|
-
<pre className="bg-zinc-100 dark:bg-zinc-800 p-4 rounded-lg text-sm text-zinc-600 dark:text-zinc-300 overflow-auto max-h-48">
|
|
46
|
-
{installProgress}
|
|
47
|
-
</pre>
|
|
48
|
-
)}
|
|
49
149
|
</div>
|
|
50
150
|
) : (
|
|
51
151
|
<button
|
|
@@ -73,18 +173,20 @@ export function InstallClaudeScreen({
|
|
|
73
173
|
)}
|
|
74
174
|
</div>
|
|
75
175
|
|
|
76
|
-
{/* Info Section */}
|
|
77
|
-
|
|
78
|
-
<div className="
|
|
79
|
-
<p>
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
176
|
+
{/* Info Section - only show when idle */}
|
|
177
|
+
{!isInstalling && !isSuccess && (
|
|
178
|
+
<div className="pt-8 space-y-4">
|
|
179
|
+
<div className="border border-zinc-200 dark:border-zinc-700 rounded-lg p-4 text-zinc-500 dark:text-zinc-400 text-sm">
|
|
180
|
+
<p>
|
|
181
|
+
<strong className="text-zinc-700 dark:text-zinc-300">What is Claude Code?</strong>
|
|
182
|
+
</p>
|
|
183
|
+
<p className="mt-2">
|
|
184
|
+
Claude Code is an AI-powered coding assistant by Anthropic.
|
|
185
|
+
JettyPod uses it to help you plan and build software.
|
|
186
|
+
</p>
|
|
187
|
+
</div>
|
|
86
188
|
</div>
|
|
87
|
-
|
|
189
|
+
)}
|
|
88
190
|
</div>
|
|
89
191
|
</div>
|
|
90
192
|
);
|