jettypod 4.4.116 → 4.4.120

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 (162) hide show
  1. package/.env +7 -0
  2. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
  3. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
  4. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
  5. package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
  6. package/apps/dashboard/app/api/usage/route.ts +17 -0
  7. package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
  8. package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
  9. package/apps/dashboard/app/connect-claude/page.tsx +24 -0
  10. package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
  11. package/apps/dashboard/app/demo/gates/page.tsx +42 -42
  12. package/apps/dashboard/app/design-system/page.tsx +868 -0
  13. package/apps/dashboard/app/globals.css +6 -2
  14. package/apps/dashboard/app/install-claude/page.tsx +9 -7
  15. package/apps/dashboard/app/layout.tsx +17 -5
  16. package/apps/dashboard/app/login/page.tsx +250 -0
  17. package/apps/dashboard/app/page.tsx +11 -9
  18. package/apps/dashboard/app/settings/page.tsx +4 -2
  19. package/apps/dashboard/app/signup/page.tsx +245 -0
  20. package/apps/dashboard/app/subscribe/page.tsx +11 -0
  21. package/apps/dashboard/app/welcome/page.tsx +24 -1
  22. package/apps/dashboard/app/work/[id]/page.tsx +34 -50
  23. package/apps/dashboard/components/AppShell.tsx +95 -55
  24. package/apps/dashboard/components/CardMenu.tsx +56 -13
  25. package/apps/dashboard/components/ClaudePanel.tsx +301 -582
  26. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
  27. package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
  28. package/apps/dashboard/components/CopyableId.tsx +3 -3
  29. package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
  30. package/apps/dashboard/components/DragContext.tsx +75 -65
  31. package/apps/dashboard/components/DraggableCard.tsx +6 -46
  32. package/apps/dashboard/components/DropZone.tsx +2 -2
  33. package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
  34. package/apps/dashboard/components/EditableTitle.tsx +26 -6
  35. package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
  36. package/apps/dashboard/components/EpicGroup.tsx +329 -0
  37. package/apps/dashboard/components/GateCard.tsx +100 -16
  38. package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
  39. package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
  40. package/apps/dashboard/components/JettyLoader.tsx +38 -0
  41. package/apps/dashboard/components/KanbanBoard.tsx +147 -766
  42. package/apps/dashboard/components/KanbanCard.tsx +506 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
  44. package/apps/dashboard/components/MainNav.tsx +20 -54
  45. package/apps/dashboard/components/MessageBlock.tsx +391 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -15
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
  53. package/apps/dashboard/components/ReviewFooter.tsx +141 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -18
  55. package/apps/dashboard/components/SubscribeContent.tsx +206 -0
  56. package/apps/dashboard/components/TestTree.tsx +15 -14
  57. package/apps/dashboard/components/TipCard.tsx +177 -0
  58. package/apps/dashboard/components/Toast.tsx +5 -5
  59. package/apps/dashboard/components/TypeIcon.tsx +56 -0
  60. package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
  62. package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
  64. package/apps/dashboard/components/WorkItemTree.tsx +9 -28
  65. package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
  66. package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
  67. package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
  68. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
  69. package/apps/dashboard/components/ui/Button.tsx +104 -0
  70. package/apps/dashboard/components/ui/Input.tsx +78 -0
  71. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
  72. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
  73. package/apps/dashboard/contexts/UsageContext.tsx +155 -0
  74. package/apps/dashboard/contexts/usageHelpers.js +9 -0
  75. package/apps/dashboard/electron/ipc-handlers.js +281 -88
  76. package/apps/dashboard/electron/main.js +691 -131
  77. package/apps/dashboard/electron/preload.js +25 -4
  78. package/apps/dashboard/electron/session-manager.js +163 -0
  79. package/apps/dashboard/electron-builder.config.js +3 -5
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/lib/backlog-parser.ts +50 -0
  83. package/apps/dashboard/lib/claude-process-manager.ts +50 -11
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/db-bridge.ts +33 -0
  86. package/apps/dashboard/lib/db.ts +136 -20
  87. package/apps/dashboard/lib/kanban-utils.ts +70 -0
  88. package/apps/dashboard/lib/run-migrations.js +27 -2
  89. package/apps/dashboard/lib/session-state-machine.ts +3 -0
  90. package/apps/dashboard/lib/session-stream-manager.ts +144 -38
  91. package/apps/dashboard/lib/shadows.ts +7 -0
  92. package/apps/dashboard/lib/tests.ts +3 -1
  93. package/apps/dashboard/lib/utils.ts +6 -0
  94. package/apps/dashboard/next.config.js +35 -14
  95. package/apps/dashboard/package.json +6 -3
  96. package/apps/dashboard/public/bug-icon.svg +9 -0
  97. package/apps/dashboard/public/buoy-icon.svg +9 -0
  98. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  99. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  100. package/apps/dashboard/public/in-flight-seagull.svg +9 -0
  101. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  102. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  103. package/apps/dashboard/public/jettypod_logo.png +0 -0
  104. package/apps/dashboard/public/pier-icon.svg +14 -0
  105. package/apps/dashboard/public/star-icon.svg +9 -0
  106. package/apps/dashboard/public/wrench-icon.svg +9 -0
  107. package/apps/dashboard/scripts/upload-to-r2.js +89 -0
  108. package/apps/dashboard/scripts/ws-server.js +191 -0
  109. package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
  110. package/apps/update-server/package.json +16 -0
  111. package/apps/update-server/schema.sql +31 -0
  112. package/apps/update-server/src/index.ts +1085 -0
  113. package/apps/update-server/tsconfig.json +16 -0
  114. package/apps/update-server/wrangler.toml +35 -0
  115. package/cucumber.js +9 -3
  116. package/docs/COMMAND_REFERENCE.md +34 -0
  117. package/hooks/post-checkout +32 -75
  118. package/hooks/post-merge +111 -10
  119. package/jest.setup.js +1 -0
  120. package/jettypod.js +54 -116
  121. package/lib/chore-taxonomy.js +33 -10
  122. package/lib/database.js +36 -16
  123. package/lib/db-watcher.js +1 -1
  124. package/lib/git-hooks/pre-commit +1 -1
  125. package/lib/jettypod-backup.js +27 -4
  126. package/lib/migrations/027-plan-at-creation-column.js +33 -0
  127. package/lib/migrations/028-ready-for-review-column.js +27 -0
  128. package/lib/migrations/029-remove-autoincrement.js +307 -0
  129. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  130. package/lib/migrations/index.js +47 -4
  131. package/lib/schema.js +13 -6
  132. package/lib/seed-onboarding.js +101 -69
  133. package/lib/update-command/index.js +9 -175
  134. package/lib/work-commands/index.js +129 -16
  135. package/lib/work-tracking/index.js +86 -46
  136. package/lib/worktree-diagnostics.js +16 -16
  137. package/lib/worktree-facade.js +1 -1
  138. package/lib/worktree-manager.js +8 -8
  139. package/lib/worktree-reconciler.js +5 -5
  140. package/package.json +9 -2
  141. package/scripts/ndjson-to-cucumber-json.js +152 -0
  142. package/scripts/postinstall.js +25 -0
  143. package/skills-templates/bug-mode/SKILL.md +39 -28
  144. package/skills-templates/bug-planning/SKILL.md +25 -29
  145. package/skills-templates/chore-mode/SKILL.md +131 -68
  146. package/skills-templates/chore-mode/verification.js +51 -10
  147. package/skills-templates/chore-planning/SKILL.md +47 -18
  148. package/skills-templates/epic-planning/SKILL.md +68 -48
  149. package/skills-templates/external-transition/SKILL.md +47 -47
  150. package/skills-templates/feature-planning/SKILL.md +83 -73
  151. package/skills-templates/production-mode/SKILL.md +49 -49
  152. package/skills-templates/request-routing/SKILL.md +27 -14
  153. package/skills-templates/simple-improvement/SKILL.md +68 -44
  154. package/skills-templates/speed-mode/SKILL.md +209 -128
  155. package/skills-templates/stable-mode/SKILL.md +105 -94
  156. package/templates/bdd-guidance.md +139 -0
  157. package/templates/bdd-scaffolding/wait.js +18 -0
  158. package/templates/bdd-scaffolding/world.js +19 -0
  159. package/.jettypod-backup/work.db +0 -0
  160. package/apps/dashboard/app/access-code/page.tsx +0 -110
  161. package/lib/discovery-checkpoint.js +0 -123
  162. package/skills-templates/project-discovery/SKILL.md +0 -372
@@ -0,0 +1,506 @@
1
+ 'use client';
2
+
3
+ import { useState, memo } from 'react';
4
+ import Link from 'next/link';
5
+ import { useRouter } from 'next/navigation';
6
+ import { m } from 'framer-motion';
7
+ import type { WorkItem } from '@/lib/db';
8
+ import { EditableTitle } from './EditableTitle';
9
+ import { CardMenu } from './CardMenu';
10
+ import { CopyableId } from './CopyableId';
11
+ import { WaveCompletionAnimation } from './WaveCompletionAnimation';
12
+ import { Input } from '@/components/ui/Input';
13
+ import { Button } from '@/components/ui/Button';
14
+ import { MODE_LABELS } from '@/lib/constants';
15
+ import { TypeIcon } from './TypeIcon';
16
+ import { shadow } from '@/lib/shadows';
17
+
18
+ function getModeLabel(item: WorkItem): string {
19
+ if (!item.mode) return '';
20
+ const base = MODE_LABELS[item.mode]?.label || item.mode;
21
+ if (item.current_step && item.total_steps) {
22
+ return `${base} ${item.current_step}/${item.total_steps}`;
23
+ }
24
+ return base;
25
+ }
26
+
27
+ // Session indicator icon - shows when item has an active Claude session
28
+ function SessionIndicatorIcon({ className }: { className?: string }) {
29
+ return (
30
+ <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
31
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
32
+ </svg>
33
+ );
34
+ }
35
+
36
+ export interface KanbanCardProps {
37
+ item: WorkItem;
38
+ epicTitle?: string | null;
39
+ showEpic?: boolean;
40
+ isInFlight?: boolean;
41
+ onTitleSave?: (id: number, newTitle: string) => Promise<void>;
42
+ onStatusChange?: (id: number, newStatus: string) => Promise<void | { success: boolean; notFound?: boolean }>;
43
+ onReject?: (id: number, reason: string) => Promise<void>;
44
+ onTriggerClaude?: (id: number, title: string, type: string, conversational?: boolean, description?: string | null) => void;
45
+ hasActiveSession?: boolean;
46
+ onOpenSession?: (id: string) => void;
47
+ onCloseSession?: (id: string) => void;
48
+ onRestart?: (id: number) => void;
49
+ usageAllowed?: boolean;
50
+ // Animation state lifted to board level
51
+ isCompletingAnimation?: boolean;
52
+ onAnimationComplete?: () => void;
53
+ isHighlighted?: boolean;
54
+ }
55
+
56
+ export const KanbanCard = memo(function KanbanCard({ item, epicTitle, showEpic = false, isInFlight = false, onTitleSave, onStatusChange, onReject, onTriggerClaude, hasActiveSession, onOpenSession, onCloseSession, onRestart, usageAllowed = true, isCompletingAnimation = false, onAnimationComplete, isHighlighted = false }: KanbanCardProps) {
57
+ const [expanded, setExpanded] = useState(false);
58
+ const [showRejectInput, setShowRejectInput] = useState(false);
59
+ const [rejectReason, setRejectReason] = useState('');
60
+ const [isEditingTitle, setIsEditingTitle] = useState(false);
61
+ const router = useRouter();
62
+
63
+ const handleOpenSession = (e: React.MouseEvent) => {
64
+ e.stopPropagation(); // Prevent card navigation
65
+ if (onOpenSession) {
66
+ onOpenSession(String(item.id));
67
+ }
68
+ };
69
+
70
+ const handleStart = async (e: React.MouseEvent) => {
71
+ e.stopPropagation(); // Prevent card navigation
72
+ if (onStatusChange) {
73
+ await onStatusChange(item.id, 'in_progress');
74
+ if (onTriggerClaude) {
75
+ onTriggerClaude(item.id, item.title, item.type, !!item.conversational, item.description);
76
+ }
77
+ }
78
+ };
79
+
80
+ const handleRestart = (e: React.MouseEvent) => {
81
+ e.stopPropagation();
82
+ if (onRestart) {
83
+ onRestart(item.id);
84
+ }
85
+ };
86
+
87
+ const canStart = item.status === 'backlog' || item.status === 'cancelled';
88
+
89
+ // An item is reviewable when it has ready_for_review flag set
90
+ // This applies to kanban-visible items: features, standalone chores/bugs, and items under epics
91
+ const isReviewable = !!item.ready_for_review;
92
+
93
+ const handleAccept = async (e: React.MouseEvent) => {
94
+ e.stopPropagation();
95
+ if (onStatusChange) {
96
+ await onStatusChange(item.id, 'done');
97
+ }
98
+ if (onCloseSession) {
99
+ onCloseSession(String(item.id));
100
+ }
101
+ };
102
+
103
+ const handleRejectClick = (e: React.MouseEvent) => {
104
+ e.stopPropagation();
105
+ setShowRejectInput(true);
106
+ };
107
+
108
+ const handleRejectConfirm = async (e: React.MouseEvent) => {
109
+ e.stopPropagation();
110
+ if (onReject && rejectReason.trim()) {
111
+ await onReject(item.id, rejectReason.trim());
112
+ setShowRejectInput(false);
113
+ setRejectReason('');
114
+ }
115
+ };
116
+
117
+ const handleRejectCancel = (e: React.MouseEvent) => {
118
+ e.stopPropagation();
119
+ setShowRejectInput(false);
120
+ setRejectReason('');
121
+ };
122
+
123
+ // Calculate chores for expandable section
124
+ const allChores = item.chores || [];
125
+ const incompleteChores = allChores.filter(c => c.status !== 'done');
126
+ const hasChores = allChores.length > 0;
127
+ const hasIncompleteChores = incompleteChores.length > 0;
128
+
129
+ // Calculate bugs for expandable section
130
+ const allBugs = item.bugs || [];
131
+ const incompleteBugs = allBugs.filter(b => b.status !== 'done');
132
+ const hasBugs = allBugs.length > 0;
133
+
134
+ const handleCardClick = () => {
135
+ router.push(`/work/${item.id}`);
136
+ };
137
+
138
+ const handleTitleSave = async (id: number, newTitle: string) => {
139
+ if (onTitleSave) {
140
+ await onTitleSave(id, newTitle);
141
+ }
142
+ };
143
+
144
+ // Status changes are now handled by the board-level wrapper that triggers animation
145
+ const handleStatusChange = async (id: number, newStatus: string) => {
146
+ if (onStatusChange) {
147
+ await onStatusChange(id, newStatus);
148
+ }
149
+ };
150
+
151
+ const isDone = item.status === 'done';
152
+
153
+ const getCardStyles = () => {
154
+ return {
155
+ className: 'bg-white dark:bg-zinc-800',
156
+ boxShadow: shadow.sm,
157
+ hoverBoxShadow: shadow.lg,
158
+ };
159
+ };
160
+
161
+ const cardStyles = getCardStyles();
162
+
163
+ const cardContent = (
164
+ <WaveCompletionAnimation isPlaying={isCompletingAnimation} onComplete={onAnimationComplete || (() => {})}>
165
+ <div
166
+ className={`rounded-xl overflow-hidden transition-[color,background-color,border-color,box-shadow,transform,translate,opacity] duration-200 ease-out hover:-translate-y-0.5 ${cardStyles.className}`}
167
+ style={{ boxShadow: cardStyles.boxShadow }}
168
+ onMouseEnter={(e) => { e.currentTarget.style.boxShadow = cardStyles.hoverBoxShadow; }}
169
+ onMouseLeave={(e) => { e.currentTarget.style.boxShadow = cardStyles.boxShadow; }}
170
+ data-testid={`kanban-card-${item.id}`}>
171
+ <div
172
+ onClick={handleCardClick}
173
+ className="block p-4 cursor-pointer"
174
+ >
175
+ <div className="flex items-start gap-3">
176
+ <span className="text-base flex-shrink-0"><TypeIcon type={item.type} /></span>
177
+ <div className="flex-1 min-w-0">
178
+ {isDone ? (
179
+ /* Compact layout for done cards: ID and title inline, no mode badge */
180
+ <div className="flex items-start gap-3">
181
+ <CopyableId id={item.id} title={item.title} type={item.type} />
182
+ {hasActiveSession && (
183
+ <button
184
+ onClick={handleOpenSession}
185
+ className="p-1 rounded hover:bg-zinc-100 dark:hover:bg-zinc-700 text-[#819D9F] transition-colors duration-200 ease-out"
186
+ aria-label="Open active session"
187
+ data-testid={`session-indicator-${item.id}`}
188
+ title="Open session"
189
+ >
190
+ <SessionIndicatorIcon className="w-4 h-4" />
191
+ </button>
192
+ )}
193
+ <span className="text-base font-medium text-zinc-900 dark:text-zinc-100">
194
+ {item.title || <span className="text-zinc-400 italic">(Untitled)</span>}
195
+ </span>
196
+ </div>
197
+ ) : (
198
+ /* Standard layout: ID + mode badge on line 1, title below */
199
+ <>
200
+ <div className="flex items-start justify-between mb-1.5">
201
+ <div className="flex items-center gap-3 flex-wrap">
202
+ <CopyableId id={item.id} title={item.title} type={item.type} />
203
+ {hasActiveSession && (
204
+ <button
205
+ onClick={handleOpenSession}
206
+ className="p-1 rounded hover:bg-zinc-100 dark:hover:bg-zinc-700 text-[#819D9F] transition-colors duration-200 ease-out"
207
+ aria-label="Open active session"
208
+ data-testid={`session-indicator-${item.id}`}
209
+ title="Open session"
210
+ >
211
+ <SessionIndicatorIcon className="w-4 h-4" />
212
+ </button>
213
+ )}
214
+ {item.mode && MODE_LABELS[item.mode] && (
215
+ <span className={`text-xs px-2 py-1 rounded ${MODE_LABELS[item.mode].color}`}>
216
+ {getModeLabel(item)}
217
+ </span>
218
+ )}
219
+ </div>
220
+ {isReviewable ? (
221
+ <div className="flex items-center gap-1.5 flex-shrink-0">
222
+ {item.status !== 'done' && onStatusChange && (
223
+ <Button
224
+ onClick={handleAccept}
225
+ variant="secondary"
226
+ size="xs"
227
+ aria-label="Accept work item"
228
+ data-testid={`accept-button-${item.id}`}
229
+ >
230
+ Accept
231
+ </Button>
232
+ )}
233
+ {onReject && (
234
+ <Button
235
+ onClick={handleRejectClick}
236
+ variant="secondary"
237
+ size="xs"
238
+ aria-label="Reject work item"
239
+ data-testid={`reject-button-${item.id}`}
240
+ >
241
+ Reject
242
+ </Button>
243
+ )}
244
+ </div>
245
+ ) : item.rejection_reason && !isDone ? (
246
+ <div className="flex items-center gap-1.5 flex-shrink-0">
247
+ <span
248
+ className="text-xs px-2 py-1 rounded bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
249
+ data-testid={`rejected-badge-${item.id}`}
250
+ >
251
+ Rejected
252
+ </span>
253
+ {!hasActiveSession && onRestart && (
254
+ <Button
255
+ onClick={handleRestart}
256
+ variant="secondary"
257
+ size="xs"
258
+ aria-label="Restart work on rejected item"
259
+ data-testid={`restart-button-${item.id}`}
260
+ >
261
+ restart
262
+ </Button>
263
+ )}
264
+ </div>
265
+ ) : null}
266
+ {canStart && onStatusChange && (
267
+ <Button
268
+ onClick={handleStart}
269
+ variant="secondary"
270
+ size="xs"
271
+ aria-label="Start work"
272
+ data-testid={`start-button-${item.id}`}
273
+ >
274
+ Start
275
+ </Button>
276
+ )}
277
+ </div>
278
+ <EditableTitle
279
+ title={item.title}
280
+ itemId={item.id}
281
+ onSave={handleTitleSave}
282
+ clickToEdit={false}
283
+ isEditing={isEditingTitle}
284
+ onEditingChange={setIsEditingTitle}
285
+ />
286
+ </>
287
+ )}
288
+ {showEpic && epicTitle && (() => {
289
+ const epicId = item.parent_id || item.epic_id;
290
+ return epicId ? (
291
+ <Link
292
+ href={`/work/${epicId}`}
293
+ onClick={(e) => e.stopPropagation()}
294
+ className="group/epic text-sm text-zinc-400 dark:text-zinc-500 mt-2 flex items-center gap-1.5 hover:text-zinc-600 dark:hover:text-zinc-300"
295
+ >
296
+ <TypeIcon type="epic" className="w-5 h-5 inline" />
297
+ <span className="group-hover/epic:underline">{epicTitle}</span>
298
+ </Link>
299
+ ) : (
300
+ <p className="text-sm text-zinc-400 dark:text-zinc-500 mt-2 flex items-center gap-1.5">
301
+ <TypeIcon type="epic" className="w-5 h-5 inline" />
302
+ <span>{epicTitle}</span>
303
+ </p>
304
+ );
305
+ })()}
306
+ </div>
307
+ <div className="flex items-center gap-1.5">
308
+ {onStatusChange && (
309
+ <CardMenu
310
+ itemId={item.id}
311
+ itemTitle={item.title}
312
+ itemType={item.type}
313
+ itemDescription={item.description}
314
+ conversational={!!item.conversational}
315
+ currentStatus={item.status}
316
+ onStatusChange={handleStatusChange}
317
+ onTriggerClaude={onTriggerClaude}
318
+ hasActiveSession={hasActiveSession}
319
+ onOpenSession={onOpenSession}
320
+ usageAllowed={usageAllowed}
321
+ onEditName={() => setIsEditingTitle(true)}
322
+ onUnaccept={item.status === 'done' && onReject ? () => setShowRejectInput(true) : undefined}
323
+ />
324
+ )}
325
+ </div>
326
+ </div>
327
+ </div>
328
+ {/* Rejection reason input */}
329
+ {showRejectInput && (
330
+ <div className="px-4 pb-4 border-t border-zinc-200 dark:border-zinc-700" onClick={(e) => e.stopPropagation()}>
331
+ <div className="mt-3">
332
+ <Input
333
+ type="text"
334
+ value={rejectReason}
335
+ onChange={(e) => setRejectReason(e.target.value)}
336
+ onKeyDown={(e) => {
337
+ e.stopPropagation();
338
+ if (e.key === 'Enter' && rejectReason.trim()) {
339
+ handleRejectConfirm(e as unknown as React.MouseEvent);
340
+ }
341
+ if (e.key === 'Escape') {
342
+ handleRejectCancel(e as unknown as React.MouseEvent);
343
+ }
344
+ }}
345
+ placeholder="Rejection reason..."
346
+ size="sm"
347
+ error
348
+ autoFocus
349
+ data-testid={`reject-reason-input-${item.id}`}
350
+ />
351
+ <div className="flex items-center gap-1.5 mt-2">
352
+ <Button
353
+ onClick={handleRejectConfirm}
354
+ disabled={!rejectReason.trim()}
355
+ variant="destructive"
356
+ size="sm"
357
+ data-testid={`reject-confirm-${item.id}`}
358
+ >
359
+ Reject
360
+ </Button>
361
+ <Button
362
+ onClick={handleRejectCancel}
363
+ variant="ghost"
364
+ size="sm"
365
+ data-testid={`reject-cancel-${item.id}`}
366
+ >
367
+ Cancel
368
+ </Button>
369
+ </div>
370
+ </div>
371
+ </div>
372
+ )}
373
+ {/* Rejected indicator */}
374
+ {item.rejection_reason && (
375
+ <div className="px-4 pb-3 border-t border-red-200 dark:border-red-800">
376
+ <div className="mt-2 flex items-start gap-2 text-base text-red-600 dark:text-red-400">
377
+ <span className="flex-shrink-0">⚠️</span>
378
+ <span className="italic">{item.rejection_reason}</span>
379
+ </div>
380
+ </div>
381
+ )}
382
+ {/* Show expandable section for features with chores or bugs */}
383
+ {(hasChores || hasBugs) && (
384
+ <div className={"border-t border-zinc-200 dark:border-zinc-700"}>
385
+ <button
386
+ onClick={() => setExpanded(!expanded)}
387
+ className={`w-full px-4 py-2 flex items-start gap-2 text-base transition-colors duration-200 ease-out ${
388
+ isDone
389
+ ? 'text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-700/50'
390
+ : 'text-zinc-600 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700/50'
391
+ }`}
392
+ >
393
+ <span className="mt-0.5">{expanded ? '▼' : '▶'}</span>
394
+ <div className="flex flex-col gap-1">
395
+ {hasChores && (
396
+ <div className="flex items-center gap-2">
397
+ <TypeIcon type="chore" />
398
+ <span>
399
+ {isDone
400
+ ? `${allChores.length === 0 ? 'no' : allChores.length} chore${allChores.length !== 1 ? 's' : ''}`
401
+ : `${incompleteChores.length === 0 ? 'no' : incompleteChores.length}${item.mode ? ` ${item.mode} mode` : ''} chore${incompleteChores.length !== 1 ? 's' : ''} left`}
402
+ </span>
403
+ </div>
404
+ )}
405
+ {hasBugs && (
406
+ <div className="flex items-center gap-2">
407
+ <TypeIcon type="bug" />
408
+ <span>
409
+ {isDone
410
+ ? `${allBugs.length === 0 ? 'no' : allBugs.length} bug${allBugs.length !== 1 ? 's' : ''}`
411
+ : `${incompleteBugs.length === 0 ? 'no' : incompleteBugs.length} bug${incompleteBugs.length !== 1 ? 's' : ''} left`}
412
+ </span>
413
+ </div>
414
+ )}
415
+ </div>
416
+ </button>
417
+ {expanded && (
418
+ <div className="px-4 pb-3 space-y-1.5">
419
+ {allChores.map((chore) => {
420
+ const isComplete = chore.status === 'done';
421
+ return (
422
+ <Link
423
+ key={chore.id}
424
+ href={`/work/${chore.id}`}
425
+ className={`block py-1.5 px-3 text-base rounded transition-colors duration-200 ease-out ${
426
+ isComplete
427
+ ? 'bg-zinc-100 dark:bg-zinc-800/50'
428
+ : 'hover:bg-zinc-100 dark:hover:bg-zinc-700'
429
+ }`}
430
+ >
431
+ <div className="flex items-center gap-3">
432
+ <span className={`font-mono ${isComplete ? 'text-zinc-500' : 'text-zinc-400'}`}>#{chore.id}</span>
433
+ {!isDone && chore.mode && MODE_LABELS[chore.mode] && (
434
+ <span className={`px-1 py-0.5 rounded text-[10px] ${MODE_LABELS[chore.mode].color}`}>
435
+ {getModeLabel(chore)}
436
+ </span>
437
+ )}
438
+ <span className={`truncate ${
439
+ isComplete
440
+ ? 'text-zinc-500'
441
+ : 'text-zinc-700 dark:text-zinc-300'
442
+ }`}>
443
+ {chore.title || <span className="text-zinc-400 italic">(Untitled)</span>}
444
+ </span>
445
+ </div>
446
+ </Link>
447
+ );
448
+ })}
449
+ {allBugs.map((bug) => {
450
+ const isComplete = bug.status === 'done';
451
+ return (
452
+ <Link
453
+ key={bug.id}
454
+ href={`/work/${bug.id}`}
455
+ className={`block py-1.5 px-3 text-base rounded transition-colors duration-200 ease-out ${
456
+ isComplete
457
+ ? 'bg-zinc-100 dark:bg-zinc-800/50'
458
+ : 'hover:bg-zinc-100 dark:hover:bg-zinc-700'
459
+ }`}
460
+ >
461
+ <div className="flex items-center gap-3">
462
+ <span className={`font-mono ${isComplete ? 'text-zinc-500' : 'text-zinc-400'}`}>#{bug.id}</span>
463
+ <TypeIcon type="bug" />
464
+ <span className={`truncate ${
465
+ isComplete
466
+ ? 'text-zinc-500'
467
+ : 'text-zinc-700 dark:text-zinc-300'
468
+ }`}>
469
+ {bug.title || <span className="text-zinc-400 italic">(Untitled)</span>}
470
+ </span>
471
+ </div>
472
+ </Link>
473
+ );
474
+ })}
475
+ </div>
476
+ )}
477
+ </div>
478
+ )}
479
+ </div>
480
+ </WaveCompletionAnimation>
481
+ );
482
+
483
+ if (isHighlighted) {
484
+ return (
485
+ <m.div
486
+ animate={{
487
+ boxShadow: [
488
+ '0 0 0 0px rgba(129, 157, 159, 0)',
489
+ '0 0 0 3px rgba(129, 157, 159, 0.4)',
490
+ '0 0 0 0px rgba(129, 157, 159, 0)',
491
+ ],
492
+ }}
493
+ transition={{
494
+ duration: 2,
495
+ repeat: Infinity,
496
+ ease: 'easeInOut',
497
+ }}
498
+ className="rounded-xl"
499
+ >
500
+ {cardContent}
501
+ </m.div>
502
+ );
503
+ }
504
+
505
+ return cardContent;
506
+ });
@@ -0,0 +1,12 @@
1
+ 'use client';
2
+
3
+ import ReactMarkdown, { type Components } from 'react-markdown';
4
+ import remarkGfm from 'remark-gfm';
5
+
6
+ export default function LazyMarkdown({ children, components }: { children: string; components?: Components }) {
7
+ return (
8
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
9
+ {children}
10
+ </ReactMarkdown>
11
+ );
12
+ }
@@ -3,9 +3,9 @@
3
3
  import Image from 'next/image';
4
4
  import Link from 'next/link';
5
5
  import { usePathname } from 'next/navigation';
6
- import { useClaudeSession } from '../contexts/ClaudeSessionContext';
7
- import { useConnectionStatus } from '../contexts/ConnectionStatusContext';
6
+ import { useSessionActions } from '../contexts/ClaudeSessionContext';
8
7
  import { ProjectSwitcher } from './ProjectSwitcher';
8
+ import { Button } from '@/components/ui/Button';
9
9
 
10
10
  interface MainNavProps {
11
11
  projectName: string;
@@ -13,8 +13,7 @@ interface MainNavProps {
13
13
 
14
14
  export function MainNav({ projectName }: MainNavProps) {
15
15
  const pathname = usePathname();
16
- const { openSessionPanel } = useClaudeSession();
17
- const { status: connectionStatus } = useConnectionStatus();
16
+ const { openSessionPanel } = useSessionActions();
18
17
 
19
18
  const isBacklogActive = pathname === '/';
20
19
  const isTestsActive = pathname === '/tests';
@@ -23,108 +22,75 @@ export function MainNav({ projectName }: MainNavProps) {
23
22
 
24
23
  return (
25
24
  <header className="sticky top-0 z-10 border-b border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 flex-shrink-0">
26
- <div className="max-w-6xl mx-auto px-4 py-4">
25
+ <div className="px-5 py-5">
27
26
  <div className="flex items-center justify-between">
28
- <div className="flex items-center gap-3">
27
+ <div className="flex items-center gap-4">
29
28
  <Image
30
- src="/jettypod_wordmark.png"
29
+ src="/jettypod_logo.png"
31
30
  alt="JettyPod"
32
- width={100}
33
- height={24}
31
+ width={36}
32
+ height={36}
34
33
  priority
34
+ className="rounded-full"
35
35
  />
36
36
  <ProjectSwitcher projectName={projectName} />
37
37
  {isBacklogActive ? (
38
- <span className="px-2.5 py-1 text-sm text-zinc-900 dark:text-zinc-100 font-medium">
38
+ <span className="px-3 py-1.5 text-base text-zinc-900 dark:text-zinc-100 font-medium">
39
39
  Backlog
40
40
  </span>
41
41
  ) : (
42
42
  <Link
43
43
  href="/"
44
- className="px-2.5 py-1 text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors"
44
+ className="px-3 py-1.5 text-base text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors duration-200 ease-out"
45
45
  >
46
46
  Backlog
47
47
  </Link>
48
48
  )}
49
49
  {isTestsActive ? (
50
- <span className="px-2.5 py-1 text-sm text-zinc-900 dark:text-zinc-100 font-medium">
50
+ <span className="px-3 py-1.5 text-base text-zinc-900 dark:text-zinc-100 font-medium">
51
51
  Tests
52
52
  </span>
53
53
  ) : (
54
54
  <Link
55
55
  href="/tests"
56
- className="px-2.5 py-1 text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors"
56
+ className="px-3 py-1.5 text-base text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors duration-200 ease-out"
57
57
  >
58
58
  Tests
59
59
  </Link>
60
60
  )}
61
61
  {isPrototypesActive ? (
62
- <span className="px-2.5 py-1 text-sm text-zinc-900 dark:text-zinc-100 font-medium">
62
+ <span className="px-3 py-1.5 text-base text-zinc-900 dark:text-zinc-100 font-medium">
63
63
  Prototypes
64
64
  </span>
65
65
  ) : (
66
66
  <Link
67
67
  href="/prototypes"
68
- className="px-2.5 py-1 text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors"
68
+ className="px-3 py-1.5 text-base text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors duration-200 ease-out"
69
69
  >
70
70
  Prototypes
71
71
  </Link>
72
72
  )}
73
73
  {isSettingsActive ? (
74
- <span className="px-2.5 py-1 text-sm text-zinc-900 dark:text-zinc-100 font-medium">
74
+ <span className="px-3 py-1.5 text-base text-zinc-900 dark:text-zinc-100 font-medium">
75
75
  Settings
76
76
  </span>
77
77
  ) : (
78
78
  <Link
79
79
  href="/settings"
80
- className="px-2.5 py-1 text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors"
80
+ className="px-3 py-1.5 text-base text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors duration-200 ease-out"
81
81
  >
82
82
  Settings
83
83
  </Link>
84
84
  )}
85
- {/* Connection Status Indicator */}
86
- <div className="flex items-center gap-2" data-testid="connection-status">
87
- <span
88
- className={`w-2 h-2 rounded-full ${
89
- connectionStatus === 'connected'
90
- ? 'bg-green-500'
91
- : connectionStatus === 'reconnecting'
92
- ? 'bg-yellow-500 animate-pulse'
93
- : 'bg-red-500'
94
- }`}
95
- />
96
- <span className="text-xs text-zinc-500 dark:text-zinc-400">
97
- {connectionStatus === 'connected'
98
- ? 'Live updates active'
99
- : connectionStatus === 'reconnecting'
100
- ? 'Reconnecting...'
101
- : 'Disconnected'}
102
- </span>
103
- </div>
104
85
  </div>
105
86
  <div className="flex items-center">
106
- <button
87
+ <Button
107
88
  onClick={openSessionPanel}
108
- className="px-4 py-2 text-xs font-medium rounded-xl transition-all duration-200 hover:-translate-y-1 hover:scale-[1.01] active:translate-y-0 active:scale-100"
109
- style={{
110
- cursor: 'pointer',
111
- background: 'linear-gradient(145deg, #ffffff 0%, #faf9f7 10%, #f0f4f4 35%, #c8d9da 55%, #819D9F 90%)',
112
- color: '#3d4d4e',
113
- boxShadow: `
114
- 0 1px 1px rgba(0, 0, 0, 0.02),
115
- 0 2px 4px rgba(0, 0, 0, 0.03),
116
- 0 6px 12px rgba(0, 0, 0, 0.05),
117
- 0 12px 24px rgba(0, 0, 0, 0.06),
118
- 0 20px 40px rgba(129, 157, 159, 0.2),
119
- 0 32px 64px rgba(129, 157, 159, 0.18),
120
- inset 0 2px 4px rgba(255, 255, 255, 1),
121
- inset 0 -2px 4px rgba(129, 157, 159, 0.05)
122
- `,
123
- }}
89
+ size="sm"
124
90
  data-testid="nav-claude-sessions-button"
125
91
  >
126
92
  Claude Sessions
127
- </button>
93
+ </Button>
128
94
  </div>
129
95
  </div>
130
96
  </div>