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
|
@@ -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 {
|
|
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 } =
|
|
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="
|
|
25
|
+
<div className="px-5 py-5">
|
|
27
26
|
<div className="flex items-center justify-between">
|
|
28
|
-
<div className="flex items-center gap-
|
|
27
|
+
<div className="flex items-center gap-4">
|
|
29
28
|
<Image
|
|
30
|
-
src="/
|
|
29
|
+
src="/jettypod_logo.png"
|
|
31
30
|
alt="JettyPod"
|
|
32
|
-
width={
|
|
33
|
-
height={
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
<
|
|
87
|
+
<Button
|
|
107
88
|
onClick={openSessionPanel}
|
|
108
|
-
|
|
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
|
-
</
|
|
93
|
+
</Button>
|
|
128
94
|
</div>
|
|
129
95
|
</div>
|
|
130
96
|
</div>
|