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.
- package/.env +7 -0
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
- 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/api/work/[id]/route.ts +35 -0
- package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
- package/apps/dashboard/app/connect-claude/page.tsx +24 -0
- package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
- package/apps/dashboard/app/demo/gates/page.tsx +42 -42
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +6 -2
- package/apps/dashboard/app/install-claude/page.tsx +9 -7
- package/apps/dashboard/app/layout.tsx +17 -5
- package/apps/dashboard/app/login/page.tsx +250 -0
- package/apps/dashboard/app/page.tsx +11 -9
- package/apps/dashboard/app/settings/page.tsx +4 -2
- package/apps/dashboard/app/signup/page.tsx +245 -0
- package/apps/dashboard/app/subscribe/page.tsx +11 -0
- package/apps/dashboard/app/welcome/page.tsx +24 -1
- package/apps/dashboard/app/work/[id]/page.tsx +34 -50
- package/apps/dashboard/components/AppShell.tsx +95 -55
- package/apps/dashboard/components/CardMenu.tsx +56 -13
- package/apps/dashboard/components/ClaudePanel.tsx +301 -582
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
- package/apps/dashboard/components/CopyableId.tsx +3 -3
- package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
- package/apps/dashboard/components/DragContext.tsx +75 -65
- package/apps/dashboard/components/DraggableCard.tsx +6 -46
- package/apps/dashboard/components/DropZone.tsx +2 -2
- package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
- package/apps/dashboard/components/EditableTitle.tsx +26 -6
- package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
- package/apps/dashboard/components/EpicGroup.tsx +329 -0
- package/apps/dashboard/components/GateCard.tsx +100 -16
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
- package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
- package/apps/dashboard/components/JettyLoader.tsx +38 -0
- package/apps/dashboard/components/KanbanBoard.tsx +147 -766
- package/apps/dashboard/components/KanbanCard.tsx +506 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
- package/apps/dashboard/components/MainNav.tsx +20 -54
- package/apps/dashboard/components/MessageBlock.tsx +391 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -15
- package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
- package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
- package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
- package/apps/dashboard/components/ReviewFooter.tsx +141 -0
- package/apps/dashboard/components/SessionList.tsx +19 -18
- package/apps/dashboard/components/SubscribeContent.tsx +206 -0
- package/apps/dashboard/components/TestTree.tsx +15 -14
- package/apps/dashboard/components/TipCard.tsx +177 -0
- package/apps/dashboard/components/Toast.tsx +5 -5
- package/apps/dashboard/components/TypeIcon.tsx +56 -0
- package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
- package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
- package/apps/dashboard/components/WorkItemTree.tsx +9 -28
- package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
- package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
- package/apps/dashboard/contexts/UsageContext.tsx +155 -0
- package/apps/dashboard/contexts/usageHelpers.js +9 -0
- package/apps/dashboard/electron/ipc-handlers.js +281 -88
- package/apps/dashboard/electron/main.js +691 -131
- package/apps/dashboard/electron/preload.js +25 -4
- package/apps/dashboard/electron/session-manager.js +163 -0
- package/apps/dashboard/electron-builder.config.js +3 -5
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/lib/backlog-parser.ts +50 -0
- package/apps/dashboard/lib/claude-process-manager.ts +50 -11
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/db-bridge.ts +33 -0
- package/apps/dashboard/lib/db.ts +136 -20
- package/apps/dashboard/lib/kanban-utils.ts +70 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/session-state-machine.ts +3 -0
- package/apps/dashboard/lib/session-stream-manager.ts +144 -38
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/tests.ts +3 -1
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next.config.js +35 -14
- package/apps/dashboard/package.json +6 -3
- package/apps/dashboard/public/bug-icon.svg +9 -0
- package/apps/dashboard/public/buoy-icon.svg +9 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.svg +9 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.svg +14 -0
- package/apps/dashboard/public/star-icon.svg +9 -0
- package/apps/dashboard/public/wrench-icon.svg +9 -0
- package/apps/dashboard/scripts/upload-to-r2.js +89 -0
- package/apps/dashboard/scripts/ws-server.js +191 -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 +1085 -0
- package/apps/update-server/tsconfig.json +16 -0
- package/apps/update-server/wrangler.toml +35 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +54 -116
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/migrations/027-plan-at-creation-column.js +33 -0
- package/lib/migrations/028-ready-for-review-column.js +27 -0
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +13 -6
- package/lib/seed-onboarding.js +101 -69
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +129 -16
- package/lib/work-tracking/index.js +86 -46
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +39 -28
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +131 -68
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/epic-planning/SKILL.md +68 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +83 -73
- package/skills-templates/production-mode/SKILL.md +49 -49
- package/skills-templates/request-routing/SKILL.md +27 -14
- package/skills-templates/simple-improvement/SKILL.md +68 -44
- package/skills-templates/speed-mode/SKILL.md +209 -128
- package/skills-templates/stable-mode/SKILL.md +105 -94
- 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
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
400
|
-
Last run: {
|
|
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-
|
|
412
|
-
<
|
|
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
|
-
|
|
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-
|
|
435
|
+
<span className="text-zinc-400 text-base">No {statusFilter === 'pass' ? 'passing' : statusFilter === 'fail' ? 'failing' : 'pending'} features matching “{searchText}”</span>
|
|
428
436
|
<div className="flex gap-2 mt-2">
|
|
429
437
|
<button
|
|
430
438
|
onClick={() => setStatusFilter(null)}
|
|
431
|
-
className="text-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
463
|
+
<span className="text-zinc-400 text-base">No results for “{searchText}”</span>
|
|
456
464
|
<button
|
|
457
465
|
onClick={() => setSearchText('')}
|
|
458
|
-
className="mt-2 text-
|
|
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-
|
|
466
|
-
<span className="text-zinc-300 text-
|
|
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-
|
|
486
|
-
<span className={`text-
|
|
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-
|
|
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-
|
|
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-
|
|
508
|
-
<div className="mb-
|
|
509
|
-
<div className="flex items-start justify-between gap-
|
|
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-
|
|
520
|
+
<p className="text-base text-zinc-500 font-mono mt-1">{selectedFeature.featureFile}</p>
|
|
513
521
|
{selectedFeature.description && (
|
|
514
|
-
<p className="text-
|
|
522
|
+
<p className="text-base text-zinc-600 mt-2">{selectedFeature.description}</p>
|
|
515
523
|
)}
|
|
516
524
|
</div>
|
|
517
|
-
<
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
|
550
|
+
className={`rounded-lg ${
|
|
552
551
|
isRunning
|
|
553
|
-
? 'border-
|
|
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
|
-
: '
|
|
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-
|
|
562
|
-
<div className="flex items-center gap-
|
|
563
|
-
<span className="text-
|
|
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-
|
|
567
|
-
isRunning ? 'text-
|
|
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-
|
|
571
|
+
<div className="flex items-center gap-3">
|
|
573
572
|
{isRunning && elapsed !== undefined ? (
|
|
574
|
-
<span className="text-xs font-mono text-
|
|
573
|
+
<span className="text-xs font-mono text-[#819D9F]">{elapsed}s</span>
|
|
575
574
|
) : (
|
|
576
|
-
<
|
|
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-
|
|
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-
|
|
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-
|
|
607
|
-
<div className="mt-
|
|
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-
|
|
622
|
+
className={`text-xs font-mono py-1.5 px-3 rounded flex items-center gap-2 ${
|
|
617
623
|
stepStatus === 'running'
|
|
618
|
-
? 'bg-
|
|
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-
|
|
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-
|
|
669
|
+
<div className="px-5 pb-5">
|
|
664
670
|
{scenario.failedStep && (
|
|
665
|
-
<div className="mb-
|
|
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-
|
|
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-
|
|
686
|
-
<span className="text-zinc-300 text-
|
|
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-
|
|
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 {
|
|
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-
|
|
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-
|
|
32
|
-
<
|
|
32
|
+
<h2 className="text-base font-semibold text-white">Sessions</h2>
|
|
33
|
+
<Button
|
|
33
34
|
onClick={onNewSession}
|
|
34
|
-
|
|
35
|
+
size="sm"
|
|
35
36
|
data-testid="new-session-button"
|
|
36
37
|
>
|
|
37
38
|
New Session
|
|
38
|
-
</
|
|
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-
|
|
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
|
-
<
|
|
57
|
+
<m.button
|
|
57
58
|
onClick={() => onSelectSession(session.id)}
|
|
58
|
-
className="flex-1 px-
|
|
59
|
+
className="flex-1 px-5 py-4 text-left"
|
|
59
60
|
whileHover={{ x: 4 }}
|
|
60
61
|
>
|
|
61
|
-
<div className="flex items-center gap-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
</
|
|
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-
|
|
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-
|
|
105
|
+
hasFeature ? 'bg-[#819D9F]/20' : 'bg-zinc-800'
|
|
105
106
|
}`}>
|
|
106
107
|
<svg
|
|
107
|
-
className={`w-4 h-4 ${hasFeature ? 'text-
|
|
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"
|