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
@@ -2,7 +2,10 @@
2
2
 
3
3
  import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
4
4
  import { useWebSocket, type WebSocketMessage } from '../hooks/useWebSocket';
5
+ import { useSessionActions } from '../contexts/ClaudeSessionContext';
5
6
  import type { TestDashboardData, TestFeature } from '@/lib/tests';
7
+ import { Input } from '@/components/ui/Input';
8
+ import { Button } from '@/components/ui/Button';
6
9
 
7
10
  interface RealTimeTestsWrapperProps {
8
11
  initialData: TestDashboardData;
@@ -31,6 +34,10 @@ const statusColors = {
31
34
 
32
35
  const SELECTED_FEATURE_KEY = 'tests-selected-feature-id';
33
36
 
37
+ function parseUtcDate(dateStr: string): Date {
38
+ return new Date(dateStr.endsWith('Z') ? dateStr : dateStr.replace(' ', 'T') + 'Z');
39
+ }
40
+
34
41
  type StepStatus = 'pending' | 'running' | 'passed' | 'failed';
35
42
 
36
43
  export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps) {
@@ -49,6 +56,7 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
49
56
 
50
57
  const elapsedStartRef = useRef<Record<string, number>>({});
51
58
  const allFeaturesRef = useRef<TestFeature[]>([]);
59
+ const { createFixScenarioSession } = useSessionActions();
52
60
 
53
61
  const refreshUndefinedSteps = useCallback(() => {
54
62
  fetch('/api/tests/undefined')
@@ -342,43 +350,43 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
342
350
  <div className="flex-shrink-0 px-6 py-4 border-b border-zinc-200">
343
351
  <div className="flex gap-4">
344
352
  <div className="flex items-center gap-2 px-4 py-2 bg-zinc-100 rounded-lg">
345
- <span className="text-sm text-zinc-500">Total</span>
353
+ <span className="text-base text-zinc-500">Total</span>
346
354
  <span className="font-mono font-semibold text-zinc-900">
347
355
  {data.summary.total}
348
356
  </span>
349
357
  </div>
350
358
  <button
351
359
  onClick={() => setStatusFilter(statusFilter === 'pass' ? null : 'pass')}
352
- className={`flex items-center gap-2 px-4 py-2 bg-green-50 rounded-lg cursor-pointer transition-all ${
360
+ className={`flex items-center gap-2 px-4 py-2 bg-green-50 rounded-lg cursor-pointer transition-[color,background-color,box-shadow] duration-200 ease-out ${
353
361
  statusFilter === 'pass' ? 'ring-2 ring-green-500' : 'hover:bg-green-100'
354
362
  }`}
355
363
  data-testid="status-badge-passing"
356
364
  >
357
- <span className="text-sm text-green-600">Passing</span>
365
+ <span className="text-base text-green-600">Passing</span>
358
366
  <span className="font-mono font-semibold text-green-700">
359
367
  {data.summary.passing}
360
368
  </span>
361
369
  </button>
362
370
  <button
363
371
  onClick={() => setStatusFilter(statusFilter === 'fail' ? null : 'fail')}
364
- className={`flex items-center gap-2 px-4 py-2 bg-red-50 rounded-lg cursor-pointer transition-all ${
372
+ className={`flex items-center gap-2 px-4 py-2 bg-red-50 rounded-lg cursor-pointer transition-[color,background-color,box-shadow] duration-200 ease-out ${
365
373
  statusFilter === 'fail' ? 'ring-2 ring-red-500' : 'hover:bg-red-100'
366
374
  }`}
367
375
  data-testid="status-badge-failing"
368
376
  >
369
- <span className="text-sm text-red-600">Failing</span>
377
+ <span className="text-base text-red-600">Failing</span>
370
378
  <span className="font-mono font-semibold text-red-700">
371
379
  {data.summary.failing}
372
380
  </span>
373
381
  </button>
374
382
  <button
375
383
  onClick={() => setStatusFilter(statusFilter === 'pending' ? null : 'pending')}
376
- className={`flex items-center gap-2 px-4 py-2 bg-amber-50 rounded-lg cursor-pointer transition-all ${
384
+ className={`flex items-center gap-2 px-4 py-2 bg-amber-50 rounded-lg cursor-pointer transition-[color,background-color,box-shadow] duration-200 ease-out ${
377
385
  statusFilter === 'pending' ? 'ring-2 ring-amber-500' : 'hover:bg-amber-100'
378
386
  }`}
379
387
  data-testid="status-badge-pending"
380
388
  >
381
- <span className="text-sm text-amber-600">Pending</span>
389
+ <span className="text-base text-amber-600">Pending</span>
382
390
  <span className="font-mono font-semibold text-amber-700">
383
391
  {data.summary.pending}
384
392
  </span>
@@ -386,7 +394,7 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
386
394
  {statusFilter && (
387
395
  <button
388
396
  onClick={() => setStatusFilter(null)}
389
- className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-500 hover:text-zinc-700 rounded-lg hover:bg-zinc-100 transition-colors"
397
+ className="flex items-center gap-1 px-3 py-2 text-base text-zinc-500 hover:text-zinc-700 rounded-lg hover:bg-zinc-100 transition-colors duration-200 ease-out"
390
398
  data-testid="clear-status-filter"
391
399
  >
392
400
  <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
@@ -396,8 +404,8 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
396
404
  </button>
397
405
  )}
398
406
  {data.summary.lastRun && (
399
- <div className="flex items-center gap-2 px-4 py-2 text-sm text-zinc-500">
400
- Last run: {new Date(data.summary.lastRun).toLocaleString()}
407
+ <div className="flex items-center gap-2 px-4 py-2 text-base text-zinc-500">
408
+ Last run: {parseUtcDate(data.summary.lastRun).toLocaleString()}
401
409
  </div>
402
410
  )}
403
411
  </div>
@@ -408,13 +416,13 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
408
416
  {/* Left Sidebar - Feature List */}
409
417
  <div className="w-80 flex-shrink-0 border-r border-zinc-200 flex flex-col overflow-hidden">
410
418
  {/* Search */}
411
- <div className="p-3 border-b border-zinc-200">
412
- <input
419
+ <div className="p-4 border-b border-zinc-200">
420
+ <Input
413
421
  type="text"
414
422
  placeholder="Search features..."
415
423
  value={searchText}
416
424
  onChange={(e) => setSearchText(e.target.value)}
417
- className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-zinc-300 focus:border-transparent placeholder:text-zinc-400"
425
+ size="sm"
418
426
  />
419
427
  </div>
420
428
 
@@ -424,17 +432,17 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
424
432
  <div className="flex flex-col items-center justify-center h-full px-6 text-center">
425
433
  {statusFilter && searchText ? (
426
434
  <>
427
- <span className="text-zinc-400 text-sm">No {statusFilter === 'pass' ? 'passing' : statusFilter === 'fail' ? 'failing' : 'pending'} features matching &ldquo;{searchText}&rdquo;</span>
435
+ <span className="text-zinc-400 text-base">No {statusFilter === 'pass' ? 'passing' : statusFilter === 'fail' ? 'failing' : 'pending'} features matching &ldquo;{searchText}&rdquo;</span>
428
436
  <div className="flex gap-2 mt-2">
429
437
  <button
430
438
  onClick={() => setStatusFilter(null)}
431
- className="text-xs text-zinc-500 hover:text-zinc-700 underline"
439
+ className="text-base text-zinc-500 hover:text-zinc-700 underline"
432
440
  >
433
441
  Clear filter
434
442
  </button>
435
443
  <button
436
444
  onClick={() => setSearchText('')}
437
- className="text-xs text-zinc-500 hover:text-zinc-700 underline"
445
+ className="text-base text-zinc-500 hover:text-zinc-700 underline"
438
446
  >
439
447
  Clear search
440
448
  </button>
@@ -442,28 +450,28 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
442
450
  </>
443
451
  ) : statusFilter ? (
444
452
  <>
445
- <span className="text-zinc-400 text-sm">No {statusFilter === 'pass' ? 'passing' : statusFilter === 'fail' ? 'failing' : 'pending'} features</span>
453
+ <span className="text-zinc-400 text-base">No {statusFilter === 'pass' ? 'passing' : statusFilter === 'fail' ? 'failing' : 'pending'} features</span>
446
454
  <button
447
455
  onClick={() => setStatusFilter(null)}
448
- className="mt-2 text-xs text-zinc-500 hover:text-zinc-700 underline"
456
+ className="mt-2 text-base text-zinc-500 hover:text-zinc-700 underline"
449
457
  >
450
458
  Clear filter
451
459
  </button>
452
460
  </>
453
461
  ) : searchText ? (
454
462
  <>
455
- <span className="text-zinc-400 text-sm">No results for &ldquo;{searchText}&rdquo;</span>
463
+ <span className="text-zinc-400 text-base">No results for &ldquo;{searchText}&rdquo;</span>
456
464
  <button
457
465
  onClick={() => setSearchText('')}
458
- className="mt-2 text-xs text-zinc-500 hover:text-zinc-700 underline"
466
+ className="mt-2 text-base text-zinc-500 hover:text-zinc-700 underline"
459
467
  >
460
468
  Clear search
461
469
  </button>
462
470
  </>
463
471
  ) : (
464
472
  <>
465
- <span className="text-zinc-400 text-sm">No features found</span>
466
- <span className="text-zinc-300 text-xs mt-1">Add .feature files to get started</span>
473
+ <span className="text-zinc-400 text-base">No features found</span>
474
+ <span className="text-zinc-300 text-base mt-1">Add .feature files to get started</span>
467
475
  </>
468
476
  )}
469
477
  </div>
@@ -478,21 +486,21 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
478
486
  <button
479
487
  key={feature.id}
480
488
  onClick={() => setSelectedFeatureId(feature.id)}
481
- className={`w-full text-left px-4 py-3 border-b border-zinc-100 hover:bg-zinc-50 transition-colors ${
489
+ className={`w-full text-left px-4 py-3 border-b border-zinc-100 hover:bg-zinc-50 transition-colors duration-200 ease-out ${
482
490
  isSelected ? 'bg-zinc-100 border-l-2 border-l-zinc-900' : ''
483
491
  }`}
484
492
  >
485
- <div className="flex items-center justify-between gap-2">
486
- <span className={`text-sm font-medium truncate ${
493
+ <div className="flex items-center justify-between gap-3">
494
+ <span className={`text-base font-medium truncate ${
487
495
  status === 'fail' ? 'text-red-700' : 'text-zinc-900'
488
496
  }`}>
489
497
  {feature.title}
490
498
  </span>
491
- <span className={`flex-shrink-0 text-xs font-mono px-2 py-0.5 rounded-full ${statusColors[status]}`}>
499
+ <span className={`flex-shrink-0 text-xs font-mono px-2.5 py-1 rounded-full ${statusColors[status]}`}>
492
500
  {passingCount}/{totalCount}
493
501
  </span>
494
502
  </div>
495
- <div className="text-xs text-zinc-400 mt-0.5 truncate">
503
+ <div className="text-base text-zinc-400 mt-0.5 truncate">
496
504
  {feature.featureFile}
497
505
  </div>
498
506
  </button>
@@ -504,42 +512,33 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
504
512
  {/* Right Panel - Detail */}
505
513
  <div className="flex-1 overflow-y-auto">
506
514
  {selectedFeature ? (
507
- <div className="p-6">
508
- <div className="mb-6">
509
- <div className="flex items-start justify-between gap-4">
515
+ <div className="p-8">
516
+ <div className="mb-8">
517
+ <div className="flex items-start justify-between gap-5">
510
518
  <div>
511
519
  <h2 className="text-lg font-semibold text-zinc-900">{selectedFeature.title}</h2>
512
- <p className="text-sm text-zinc-500 font-mono mt-1">{selectedFeature.featureFile}</p>
520
+ <p className="text-base text-zinc-500 font-mono mt-1">{selectedFeature.featureFile}</p>
513
521
  {selectedFeature.description && (
514
- <p className="text-sm text-zinc-600 mt-2">{selectedFeature.description}</p>
522
+ <p className="text-base text-zinc-600 mt-2">{selectedFeature.description}</p>
515
523
  )}
516
524
  </div>
517
- <button
525
+ <Button
518
526
  onClick={() => {
519
527
  if (!runningAll && runningScenarios.size === 0) {
520
528
  runAllScenarios(selectedFeature);
521
529
  }
522
530
  }}
523
531
  disabled={runningAll || runningScenarios.size > 0}
524
- className={`flex-shrink-0 px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
525
- runningAll || runningScenarios.size > 0
526
- ? 'text-zinc-400 bg-zinc-50 cursor-not-allowed'
527
- : 'text-white bg-zinc-900 hover:bg-zinc-800'
528
- }`}
532
+ loading={runningAll}
533
+ size="sm"
534
+ className="flex-shrink-0"
529
535
  >
530
- {runningAll ? (
531
- <span className="flex items-center gap-2">
532
- <span className="inline-block w-3.5 h-3.5 border-2 border-zinc-300 border-t-white rounded-full animate-spin" />
533
- Running All...
534
- </span>
535
- ) : (
536
- 'Run All'
537
- )}
538
- </button>
536
+ {runningAll ? 'Running All...' : 'Run All'}
537
+ </Button>
539
538
  </div>
540
539
  </div>
541
540
 
542
- <div className="space-y-4">
541
+ <div className="space-y-5">
543
542
  {selectedFeature.scenarios.map(scenario => {
544
543
  const scenarioKey = `${selectedFeature.featureFile}::${scenario.title}`;
545
544
  const isRunning = runningScenarios.has(scenarioKey);
@@ -548,32 +547,39 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
548
547
  return (
549
548
  <div
550
549
  key={scenario.id}
551
- className={`rounded-lg border ${
550
+ className={`rounded-lg ${
552
551
  isRunning
553
- ? 'border-blue-200 bg-blue-50'
552
+ ? 'border-2 border-[#819D9F]/30 bg-[#e8f0f0]'
554
553
  : scenario.status === 'fail'
555
- ? 'border-red-200 bg-red-50'
554
+ ? 'border-2 border-red-200 bg-red-50'
556
555
  : scenario.status === 'pending'
557
- ? 'border-amber-200 bg-amber-50'
558
- : 'border-zinc-200 bg-white'
556
+ ? 'border-2 border-amber-200 bg-amber-50'
557
+ : 'bg-white'
559
558
  }`}
560
559
  >
561
- <div className="flex items-center justify-between p-4">
562
- <div className="flex items-center gap-2">
563
- <span className="text-sm">
560
+ <div className="flex items-center justify-between p-5">
561
+ <div className="flex items-center gap-3">
562
+ <span className="text-base">
564
563
  {isRunning ? '🔄' : scenario.status === 'pass' ? '✅' : scenario.status === 'fail' ? '❌' : '⏳'}
565
564
  </span>
566
- <span className={`text-sm font-medium ${
567
- isRunning ? 'text-blue-700' : scenario.status === 'fail' ? 'text-red-700' : 'text-zinc-900'
565
+ <span className={`text-base font-medium ${
566
+ isRunning ? 'text-[#5a7d7f]' : scenario.status === 'fail' ? 'text-red-700' : 'text-zinc-900'
568
567
  }`}>
569
568
  {scenario.title}
570
569
  </span>
571
570
  </div>
572
- <div className="flex items-center gap-2">
571
+ <div className="flex items-center gap-3">
573
572
  {isRunning && elapsed !== undefined ? (
574
- <span className="text-xs font-mono text-blue-500">{elapsed}s</span>
573
+ <span className="text-xs font-mono text-[#819D9F]">{elapsed}s</span>
575
574
  ) : (
576
- <span className="text-xs font-mono text-zinc-400">{scenario.duration}</span>
575
+ <div className="flex items-center gap-3">
576
+ <span className="text-xs font-mono text-zinc-400">{scenario.duration}</span>
577
+ {scenario.lastRun && (
578
+ <span className="text-xs text-zinc-400" title={parseUtcDate(scenario.lastRun).toLocaleString()}>
579
+ {parseUtcDate(scenario.lastRun).toLocaleString()}
580
+ </span>
581
+ )}
582
+ </div>
577
583
  )}
578
584
  <button
579
585
  onClick={(e) => {
@@ -583,7 +589,7 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
583
589
  }
584
590
  }}
585
591
  disabled={isRunning}
586
- className={`px-2 py-1 text-xs font-medium rounded transition-colors ${
592
+ className={`px-3 py-1.5 text-base font-medium rounded transition-colors duration-200 ease-out ${
587
593
  isRunning
588
594
  ? 'text-zinc-400 bg-zinc-50 cursor-not-allowed'
589
595
  : 'text-zinc-600 bg-zinc-100 hover:bg-zinc-200'
@@ -591,7 +597,7 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
591
597
  >
592
598
  {isRunning ? (
593
599
  <span className="flex items-center gap-1">
594
- <span className="inline-block w-3 h-3 border-2 border-blue-300 border-t-blue-600 rounded-full animate-spin" />
600
+ <span className="inline-block w-3 h-3 border-2 border-[#819D9F]/50 border-t-[#5a7d7f] rounded-full animate-spin" />
595
601
  Running...
596
602
  </span>
597
603
  ) : (
@@ -603,8 +609,8 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
603
609
 
604
610
  {/* Gherkin Steps */}
605
611
  {scenario.steps && scenario.steps.length > 0 && (
606
- <div className="px-4 pb-3 border-t border-zinc-100">
607
- <div className="mt-3 space-y-1">
612
+ <div className="px-5 pb-4 border-t border-zinc-100">
613
+ <div className="mt-4 space-y-1.5">
608
614
  {scenario.steps.map((step: string, i: number) => {
609
615
  const isFailedStep = scenario.failedStep && step.includes(scenario.failedStep.replace(/^(Given |When |Then |And |But )/, ''));
610
616
  const scenarioUndefined = undefinedStepsMap[scenario.title] || [];
@@ -613,9 +619,9 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
613
619
  return (
614
620
  <div
615
621
  key={i}
616
- className={`text-xs font-mono py-1 px-2 rounded flex items-center gap-1.5 ${
622
+ className={`text-xs font-mono py-1.5 px-3 rounded flex items-center gap-2 ${
617
623
  stepStatus === 'running'
618
- ? 'bg-blue-100 text-blue-900 border-l-2 border-blue-500'
624
+ ? 'bg-[#e8f0f0] text-zinc-900 border-l-2 border-[#819D9F]'
619
625
  : stepStatus === 'passed'
620
626
  ? 'bg-green-50 text-green-800 border-l-2 border-green-400'
621
627
  : stepStatus === 'failed'
@@ -631,7 +637,7 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
631
637
  >
632
638
  {stepStatus === 'running' && (
633
639
  <span className="flex-shrink-0">
634
- <span className="inline-block w-2.5 h-2.5 border-2 border-blue-300 border-t-blue-600 rounded-full animate-spin" />
640
+ <span className="inline-block w-2.5 h-2.5 border-2 border-[#819D9F]/50 border-t-[#5a7d7f] rounded-full animate-spin" />
635
641
  </span>
636
642
  )}
637
643
  {stepStatus === 'passed' && (
@@ -660,17 +666,34 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
660
666
 
661
667
  {/* Error Details */}
662
668
  {scenario.status === 'fail' && scenario.error && (
663
- <div className="px-4 pb-4">
669
+ <div className="px-5 pb-5">
664
670
  {scenario.failedStep && (
665
- <div className="mb-2 text-xs font-medium text-red-700">
671
+ <div className="mb-3 text-base font-medium text-red-700">
666
672
  Failed at: {scenario.failedStep}
667
673
  </div>
668
674
  )}
669
- <div className="p-3 bg-red-100 rounded border border-red-200">
675
+ <div className="p-4 bg-red-100 rounded border-2 border-red-200">
670
676
  <pre className="text-xs text-red-800 whitespace-pre-wrap font-mono">
671
677
  {scenario.error}
672
678
  </pre>
673
679
  </div>
680
+ <Button
681
+ onClick={(e) => {
682
+ e.stopPropagation();
683
+ createFixScenarioSession(
684
+ selectedFeature.featureFile,
685
+ scenario.title,
686
+ scenario.error!,
687
+ scenario.failedStep,
688
+ scenario.steps
689
+ );
690
+ }}
691
+ className="mt-4"
692
+ variant="accent"
693
+ size="sm"
694
+ >
695
+ Fix with Claude
696
+ </Button>
674
697
  </div>
675
698
  )}
676
699
  </div>
@@ -682,11 +705,11 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
682
705
  <div className="flex flex-col items-center justify-center h-full text-center px-6">
683
706
  {allFeatures.length === 0 ? (
684
707
  <>
685
- <span className="text-zinc-400 text-sm">No test features available</span>
686
- <span className="text-zinc-300 text-xs mt-1">Create BDD feature files in the features/ directory to see test results here.</span>
708
+ <span className="text-zinc-400 text-base">No test features available</span>
709
+ <span className="text-zinc-300 text-base mt-1">Create BDD feature files in the features/ directory to see test results here.</span>
687
710
  </>
688
711
  ) : (
689
- <span className="text-zinc-400 text-sm">Select a feature from the sidebar to view its scenarios</span>
712
+ <span className="text-zinc-400 text-base">Select a feature from the sidebar to view its scenarios</span>
690
713
  )}
691
714
  </div>
692
715
  )}
@@ -0,0 +1,141 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Button } from '@/components/ui/Button';
5
+ import { Input } from '@/components/ui/Input';
6
+
7
+ interface ReviewFooterProps {
8
+ workItemId: string;
9
+ onAccepted: () => void;
10
+ onRejected: (reason: string) => void;
11
+ }
12
+
13
+ export function ReviewFooter({ workItemId, onAccepted, onRejected }: ReviewFooterProps) {
14
+ const [showRejectInput, setShowRejectInput] = useState(false);
15
+ const [rejectReason, setRejectReason] = useState('');
16
+ const [isSubmitting, setIsSubmitting] = useState(false);
17
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
18
+
19
+ const handleAccept = async () => {
20
+ setErrorMessage(null);
21
+ setIsSubmitting(true);
22
+ try {
23
+ const res = await fetch(`/api/work/${workItemId}/status`, {
24
+ method: 'PATCH',
25
+ headers: { 'Content-Type': 'application/json' },
26
+ body: JSON.stringify({ status: 'done' }),
27
+ });
28
+ if (!res.ok) throw new Error('Failed to update status');
29
+ onAccepted();
30
+ } catch {
31
+ setErrorMessage('Failed to accept. Please try again.');
32
+ setIsSubmitting(false);
33
+ }
34
+ };
35
+
36
+ const handleRejectClick = () => {
37
+ setShowRejectInput(true);
38
+ };
39
+
40
+ const handleRejectConfirm = async () => {
41
+ if (!rejectReason.trim()) return;
42
+ setErrorMessage(null);
43
+ setIsSubmitting(true);
44
+ try {
45
+ const res = await fetch(`/api/work/${workItemId}/status`, {
46
+ method: 'PATCH',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify({ status: 'in_progress', rejectionReason: rejectReason.trim() }),
49
+ });
50
+ if (!res.ok) throw new Error('Failed to update status');
51
+ const reason = rejectReason.trim();
52
+ setIsSubmitting(false);
53
+ setShowRejectInput(false);
54
+ setRejectReason('');
55
+ onRejected(reason);
56
+ } catch {
57
+ setErrorMessage('Failed to reject. Please try again.');
58
+ setIsSubmitting(false);
59
+ }
60
+ };
61
+
62
+ const handleRejectCancel = () => {
63
+ setShowRejectInput(false);
64
+ setRejectReason('');
65
+ };
66
+
67
+ return (
68
+ <div className="border-t border-zinc-200 px-5 py-5 flex-shrink-0" data-testid="review-footer">
69
+ <p className="text-base font-semibold text-zinc-900 mb-1.5">
70
+ This work is done. How does it look?
71
+ </p>
72
+ <p className="text-base text-zinc-500 mb-4">
73
+ Just a heads up. This chat will close after acceptance.
74
+ </p>
75
+
76
+ {errorMessage && (
77
+ <p className="text-sm text-red-600 mb-3" data-testid="review-error-message">{errorMessage}</p>
78
+ )}
79
+
80
+ {!showRejectInput ? (
81
+ <div className="flex items-center gap-2" data-testid="review-actions">
82
+ <Button
83
+ onClick={handleAccept}
84
+ loading={isSubmitting}
85
+ data-testid="review-accept-button"
86
+ >
87
+ Accept
88
+ </Button>
89
+ <Button
90
+ onClick={handleRejectClick}
91
+ variant="secondary"
92
+ data-testid="review-reject-button"
93
+ >
94
+ Reject
95
+ </Button>
96
+ </div>
97
+ ) : (
98
+ <div data-testid="review-reject-input-area">
99
+ <Input
100
+ type="text"
101
+ value={rejectReason}
102
+ onChange={(e) => setRejectReason(e.target.value)}
103
+ onKeyDown={(e) => {
104
+ if (e.key === 'Enter' && rejectReason.trim()) {
105
+ handleRejectConfirm();
106
+ }
107
+ if (e.key === 'Escape') {
108
+ handleRejectCancel();
109
+ }
110
+ }}
111
+ placeholder="Rejection reason..."
112
+ size="sm"
113
+ error
114
+ autoFocus
115
+ data-testid="review-reject-reason-input"
116
+ />
117
+ <div className="flex items-center gap-1.5 mt-2">
118
+ <Button
119
+ onClick={handleRejectConfirm}
120
+ disabled={!rejectReason.trim()}
121
+ loading={isSubmitting}
122
+ variant="destructive"
123
+ size="sm"
124
+ data-testid="review-reject-confirm"
125
+ >
126
+ Reject
127
+ </Button>
128
+ <Button
129
+ onClick={handleRejectCancel}
130
+ variant="ghost"
131
+ size="sm"
132
+ data-testid="review-reject-cancel"
133
+ >
134
+ Cancel
135
+ </Button>
136
+ </div>
137
+ </div>
138
+ )}
139
+ </div>
140
+ );
141
+ }
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { motion } from 'framer-motion';
3
+ import { m } from 'framer-motion';
4
+ import { Button } from '@/components/ui/Button';
4
5
 
5
6
  export interface SessionItem {
6
7
  id: string;
@@ -26,23 +27,23 @@ export function SessionList({
26
27
  return (
27
28
  <div className="flex-1 flex flex-col" data-testid="session-list">
28
29
  {/* Header */}
29
- <div className="px-4 py-3 border-b border-zinc-800">
30
+ <div className="px-5 py-4 border-b border-zinc-800">
30
31
  <div className="flex items-center justify-between">
31
- <h2 className="text-sm font-semibold text-white">Sessions</h2>
32
- <button
32
+ <h2 className="text-base font-semibold text-white">Sessions</h2>
33
+ <Button
33
34
  onClick={onNewSession}
34
- className="px-3 py-1.5 text-xs font-medium bg-blue-600 hover:bg-blue-500 text-white rounded transition-colors"
35
+ size="sm"
35
36
  data-testid="new-session-button"
36
37
  >
37
38
  New Session
38
- </button>
39
+ </Button>
39
40
  </div>
40
41
  </div>
41
42
 
42
43
  {/* Session List */}
43
44
  <div className="flex-1 overflow-y-auto">
44
45
  {sessions.length === 0 ? (
45
- <div className="p-4 text-center text-zinc-500 text-sm" data-testid="empty-session-state">
46
+ <div className="p-6 text-center text-zinc-500 text-base" data-testid="empty-session-state">
46
47
  No sessions yet. Click New Session to start.
47
48
  </div>
48
49
  ) : (
@@ -50,39 +51,39 @@ export function SessionList({
50
51
  {sessions.map((session) => (
51
52
  <div
52
53
  key={session.id}
53
- className="flex items-center hover:bg-zinc-800/50 transition-colors"
54
+ className="flex items-center hover:bg-zinc-800/50 transition-colors duration-200 ease-out"
54
55
  data-testid={`session-item-${session.id}`}
55
56
  >
56
- <motion.button
57
+ <m.button
57
58
  onClick={() => onSelectSession(session.id)}
58
- className="flex-1 px-4 py-3 text-left"
59
+ className="flex-1 px-5 py-4 text-left"
59
60
  whileHover={{ x: 4 }}
60
61
  >
61
- <div className="flex items-center gap-2">
62
+ <div className="flex items-center gap-3">
62
63
  <SessionIcon hasFeature={!!session.featureId} />
63
64
  <div className="flex-1 min-w-0">
64
- <p className="text-sm font-medium text-white truncate">
65
+ <p className="text-base font-medium text-white truncate">
65
66
  {session.featureId ? session.featureTitle : session.title}
66
67
  </p>
67
68
  {session.featureId && (
68
- <span className="inline-flex items-center px-1.5 py-0.5 mt-1 text-xs font-medium bg-blue-900/50 text-blue-300 rounded">
69
+ <span className="inline-flex items-center px-2 py-1 mt-1.5 text-xs font-medium bg-[#819D9F]/20 text-[#a3bfc0] rounded">
69
70
  #{session.featureId}
70
71
  </span>
71
72
  )}
72
73
  {!session.featureId && (
73
- <p className="text-xs text-zinc-500 mt-0.5">Unlinked session</p>
74
+ <p className="text-base text-zinc-500 mt-1">Unlinked session</p>
74
75
  )}
75
76
  </div>
76
77
  <ChevronIcon />
77
78
  </div>
78
- </motion.button>
79
+ </m.button>
79
80
  {onCloseSession && (
80
81
  <button
81
82
  onClick={(e) => {
82
83
  e.stopPropagation();
83
84
  onCloseSession(session.id);
84
85
  }}
85
- className="p-2 mr-2 rounded hover:bg-zinc-700 text-zinc-500 hover:text-zinc-300 transition-colors"
86
+ className="p-2.5 mr-3 rounded hover:bg-zinc-700 text-zinc-500 hover:text-zinc-300 transition-colors duration-200 ease-out"
86
87
  aria-label="Close session"
87
88
  data-testid={`close-session-${session.id}`}
88
89
  >
@@ -101,10 +102,10 @@ export function SessionList({
101
102
  function SessionIcon({ hasFeature }: { hasFeature: boolean }) {
102
103
  return (
103
104
  <div className={`w-8 h-8 rounded-full flex items-center justify-center ${
104
- hasFeature ? 'bg-blue-900/50' : 'bg-zinc-800'
105
+ hasFeature ? 'bg-[#819D9F]/20' : 'bg-zinc-800'
105
106
  }`}>
106
107
  <svg
107
- className={`w-4 h-4 ${hasFeature ? 'text-blue-400' : 'text-zinc-400'}`}
108
+ className={`w-4 h-4 ${hasFeature ? 'text-[#819D9F]' : 'text-zinc-400'}`}
108
109
  fill="none"
109
110
  stroke="currentColor"
110
111
  viewBox="0 0 24 24"