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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useRef, useCallback, KeyboardEvent, ChangeEvent } from 'react';
|
|
4
|
-
import {
|
|
3
|
+
import { useState, useRef, useCallback, useEffect, KeyboardEvent, ChangeEvent } from 'react';
|
|
4
|
+
import { m, AnimatePresence } from 'framer-motion';
|
|
5
5
|
|
|
6
6
|
export interface AttachedImage {
|
|
7
7
|
id: string;
|
|
@@ -19,6 +19,7 @@ interface ClaudePanelInputProps {
|
|
|
19
19
|
placeholder?: string;
|
|
20
20
|
attachedImages?: AttachedImage[];
|
|
21
21
|
onImagesChange?: (images: AttachedImage[]) => void;
|
|
22
|
+
activeSessionId?: string | null;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const DOUBLE_ESCAPE_THRESHOLD_MS = 500;
|
|
@@ -31,6 +32,7 @@ export function ClaudePanelInput({
|
|
|
31
32
|
placeholder = 'Type a message...',
|
|
32
33
|
attachedImages: externalImages,
|
|
33
34
|
onImagesChange,
|
|
35
|
+
activeSessionId,
|
|
34
36
|
}: ClaudePanelInputProps) {
|
|
35
37
|
const [message, setMessage] = useState('');
|
|
36
38
|
const [isFocused, setIsFocused] = useState(false);
|
|
@@ -42,6 +44,13 @@ export function ClaudePanelInput({
|
|
|
42
44
|
const attachedImages = externalImages ?? internalImages;
|
|
43
45
|
const setAttachedImages = onImagesChange ?? setInternalImages;
|
|
44
46
|
|
|
47
|
+
// Auto-focus textarea when active session changes (new session or tab switch)
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (activeSessionId && textareaRef.current) {
|
|
50
|
+
textareaRef.current.focus();
|
|
51
|
+
}
|
|
52
|
+
}, [activeSessionId]);
|
|
53
|
+
|
|
45
54
|
const handleSend = useCallback(() => {
|
|
46
55
|
const trimmed = message.trim();
|
|
47
56
|
const hasContent = trimmed || attachedImages.length > 0;
|
|
@@ -93,20 +102,20 @@ export function ClaudePanelInput({
|
|
|
93
102
|
|
|
94
103
|
return (
|
|
95
104
|
<div
|
|
96
|
-
className="border-t border-zinc-200 bg-zinc-50 p-
|
|
105
|
+
className="border-t border-zinc-200 bg-zinc-50 p-4"
|
|
97
106
|
data-testid="claude-panel-input"
|
|
98
107
|
>
|
|
99
108
|
<div
|
|
100
109
|
className={`
|
|
101
|
-
relative rounded-lg border transition-
|
|
102
|
-
${isFocused ? 'border-
|
|
110
|
+
relative rounded-lg border-2 transition-[border-color,box-shadow] duration-200 ease-out
|
|
111
|
+
${isFocused ? 'border-[#819D9F] bg-white' : 'border-zinc-300 bg-white'}
|
|
103
112
|
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
|
104
113
|
`}
|
|
105
114
|
>
|
|
106
115
|
|
|
107
116
|
{/* Thumbnail preview section */}
|
|
108
117
|
{attachedImages.length > 0 && (
|
|
109
|
-
<div className="flex flex-wrap gap-
|
|
118
|
+
<div className="flex flex-wrap gap-3 px-4 pt-3" data-testid="image-preview-section">
|
|
110
119
|
{attachedImages.map(image => (
|
|
111
120
|
<div
|
|
112
121
|
key={image.id}
|
|
@@ -116,12 +125,12 @@ export function ClaudePanelInput({
|
|
|
116
125
|
<img
|
|
117
126
|
src={image.dataUrl}
|
|
118
127
|
alt={image.name}
|
|
119
|
-
className="w-16 h-16 object-cover rounded
|
|
128
|
+
className="w-16 h-16 object-cover rounded"
|
|
120
129
|
/>
|
|
121
130
|
<button
|
|
122
131
|
type="button"
|
|
123
132
|
onClick={() => removeImage(image.id)}
|
|
124
|
-
className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-zinc-700 hover:bg-zinc-900 text-white rounded-full flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition-opacity"
|
|
133
|
+
className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-zinc-700 hover:bg-zinc-900 text-white rounded-full flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-out"
|
|
125
134
|
data-testid="remove-image-button"
|
|
126
135
|
aria-label={`Remove ${image.name}`}
|
|
127
136
|
>
|
|
@@ -143,10 +152,10 @@ export function ClaudePanelInput({
|
|
|
143
152
|
disabled={disabled}
|
|
144
153
|
rows={1}
|
|
145
154
|
className={`
|
|
146
|
-
w-full resize-none bg-transparent px-
|
|
155
|
+
w-full resize-none bg-transparent px-4 py-3 text-base text-zinc-900
|
|
147
156
|
placeholder:text-zinc-400 focus:outline-none
|
|
148
157
|
${isExpanded ? 'min-h-[80px]' : 'min-h-[40px]'}
|
|
149
|
-
transition-
|
|
158
|
+
transition-[min-height] duration-200 ease-out
|
|
150
159
|
`}
|
|
151
160
|
style={{ maxHeight: '200px' }}
|
|
152
161
|
data-testid="claude-input-textarea"
|
|
@@ -155,12 +164,12 @@ export function ClaudePanelInput({
|
|
|
155
164
|
{/* Footer with character count and hints - only show when expanded */}
|
|
156
165
|
<AnimatePresence>
|
|
157
166
|
{isExpanded && (
|
|
158
|
-
<
|
|
167
|
+
<m.div
|
|
159
168
|
initial={{ opacity: 0, height: 0 }}
|
|
160
169
|
animate={{ opacity: 1, height: 'auto' }}
|
|
161
170
|
exit={{ opacity: 0, height: 0 }}
|
|
162
171
|
transition={{ duration: 0.15 }}
|
|
163
|
-
className="flex items-center justify-between px-
|
|
172
|
+
className="flex items-center justify-between px-4 pb-3 text-xs text-zinc-500"
|
|
164
173
|
>
|
|
165
174
|
<div className="flex items-center gap-3" data-testid="keyboard-hints">
|
|
166
175
|
<span>
|
|
@@ -177,14 +186,14 @@ export function ClaudePanelInput({
|
|
|
177
186
|
<span>
|
|
178
187
|
<kbd className="px-1.5 py-0.5 rounded bg-zinc-200 text-zinc-600 font-mono text-[10px]">Esc</kbd>
|
|
179
188
|
<kbd className="px-1.5 py-0.5 rounded bg-zinc-200 text-zinc-600 font-mono text-[10px] ml-0.5">Esc</kbd>
|
|
180
|
-
{' '}to
|
|
189
|
+
{' '}to interrupt
|
|
181
190
|
</span>
|
|
182
191
|
)}
|
|
183
192
|
</div>
|
|
184
193
|
<span data-testid="character-count">
|
|
185
194
|
{characterCount > 0 ? `${characterCount} chars` : ''}
|
|
186
195
|
</span>
|
|
187
|
-
</
|
|
196
|
+
</m.div>
|
|
188
197
|
)}
|
|
189
198
|
</AnimatePresence>
|
|
190
199
|
</div>
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
import { Button } from '@/components/ui/Button';
|
|
6
|
+
|
|
7
|
+
type ConnectState = 'idle' | 'waiting' | 'success' | 'error';
|
|
8
|
+
|
|
9
|
+
const AUTH_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
|
|
10
|
+
|
|
11
|
+
interface ConnectClaudeScreenProps {
|
|
12
|
+
onConnect: () => Promise<{ success: boolean; error?: string }>;
|
|
13
|
+
onCheckAuth: () => Promise<boolean>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ConnectClaudeScreen({ onConnect, onCheckAuth }: ConnectClaudeScreenProps) {
|
|
17
|
+
const [state, setState] = useState<ConnectState>('idle');
|
|
18
|
+
const [completedSteps, setCompletedSteps] = useState<number[]>([]);
|
|
19
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
20
|
+
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
21
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
22
|
+
|
|
23
|
+
const cleanup = () => {
|
|
24
|
+
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
|
|
25
|
+
if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; }
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
return cleanup;
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const handleSuccess = () => {
|
|
33
|
+
cleanup();
|
|
34
|
+
setCompletedSteps([1, 2, 3]);
|
|
35
|
+
setState('success');
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
window.location.href = '/';
|
|
38
|
+
}, 1500);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleError = (message: string) => {
|
|
42
|
+
cleanup();
|
|
43
|
+
setCompletedSteps([]);
|
|
44
|
+
setErrorMessage(message);
|
|
45
|
+
setState('error');
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleConnect = async () => {
|
|
49
|
+
setState('waiting');
|
|
50
|
+
setCompletedSteps([1]);
|
|
51
|
+
setErrorMessage(null);
|
|
52
|
+
|
|
53
|
+
// Trigger claude login (opens browser)
|
|
54
|
+
let result: { success: boolean; error?: string };
|
|
55
|
+
try {
|
|
56
|
+
result = await onConnect();
|
|
57
|
+
} catch {
|
|
58
|
+
handleError('Failed to start Claude login. Please try again.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (result.success) {
|
|
63
|
+
handleSuccess();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Start polling in case login succeeds via browser after process exits
|
|
68
|
+
pollRef.current = setInterval(async () => {
|
|
69
|
+
try {
|
|
70
|
+
const authed = await onCheckAuth();
|
|
71
|
+
if (authed) {
|
|
72
|
+
handleSuccess();
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Polling check failed — ignore and retry on next interval
|
|
76
|
+
}
|
|
77
|
+
}, 2000);
|
|
78
|
+
|
|
79
|
+
// Set timeout to stop polling after AUTH_TIMEOUT_MS
|
|
80
|
+
timeoutRef.current = setTimeout(() => {
|
|
81
|
+
handleError('Authentication timed out. Make sure you completed sign-in in the browser, then try again.');
|
|
82
|
+
}, AUTH_TIMEOUT_MS);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const steps = [
|
|
86
|
+
'Open Anthropic login in browser',
|
|
87
|
+
'Sign in to your Anthropic account',
|
|
88
|
+
'Return to JettyPod — ready to go',
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900 p-8">
|
|
93
|
+
<div className="max-w-md w-full space-y-10">
|
|
94
|
+
{/* Logo */}
|
|
95
|
+
<div className="flex flex-col items-center space-y-6">
|
|
96
|
+
<Image
|
|
97
|
+
src="/jettypod_wordmark.png"
|
|
98
|
+
alt="JettyPod"
|
|
99
|
+
width={160}
|
|
100
|
+
height={40}
|
|
101
|
+
priority
|
|
102
|
+
/>
|
|
103
|
+
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
|
|
104
|
+
Connect Claude Code
|
|
105
|
+
</h1>
|
|
106
|
+
<p className="text-zinc-500 dark:text-zinc-400 text-center">
|
|
107
|
+
Claude Code needs to be connected to your Anthropic account.
|
|
108
|
+
This will open your browser to sign in.
|
|
109
|
+
</p>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Error Banner */}
|
|
113
|
+
{errorMessage && (
|
|
114
|
+
<div
|
|
115
|
+
className="bg-red-50 dark:bg-red-900/20 border-2 border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-5 py-4 rounded-xl text-base"
|
|
116
|
+
data-testid="connect-error"
|
|
117
|
+
>
|
|
118
|
+
{errorMessage}
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{/* Progress Stepper */}
|
|
123
|
+
<div className="flex flex-col gap-5">
|
|
124
|
+
{steps.map((label, i) => {
|
|
125
|
+
const stepNum = i + 1;
|
|
126
|
+
const isDone = completedSteps.includes(stepNum);
|
|
127
|
+
const isActive = !isDone && (
|
|
128
|
+
(state === 'idle' && stepNum === 1) ||
|
|
129
|
+
(state === 'waiting' && stepNum === Math.max(...completedSteps) + 1)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div
|
|
134
|
+
key={stepNum}
|
|
135
|
+
className={`flex items-center gap-4 text-base ${
|
|
136
|
+
isDone ? 'text-zinc-900 dark:text-zinc-100' :
|
|
137
|
+
isActive ? 'text-zinc-900 dark:text-zinc-100 font-medium' :
|
|
138
|
+
'text-zinc-400 dark:text-zinc-500'
|
|
139
|
+
}`}
|
|
140
|
+
>
|
|
141
|
+
<span
|
|
142
|
+
className={`w-7 h-7 rounded-full flex items-center justify-center text-base font-semibold flex-shrink-0 transition-colors duration-200 ease-out ${
|
|
143
|
+
isDone ? 'bg-green-400 text-white' :
|
|
144
|
+
isActive ? 'bg-[#c8d9da] text-[#3d4d4e]' :
|
|
145
|
+
'bg-zinc-200 dark:bg-zinc-700 text-zinc-400 dark:text-zinc-500'
|
|
146
|
+
}`}
|
|
147
|
+
>
|
|
148
|
+
{isDone ? '✓' : stepNum}
|
|
149
|
+
</span>
|
|
150
|
+
<span>{label}</span>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Connect Button */}
|
|
157
|
+
<div className="pt-6">
|
|
158
|
+
{state === 'success' ? (
|
|
159
|
+
<div
|
|
160
|
+
className="w-full py-3 px-6 rounded-xl font-medium text-center bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400"
|
|
161
|
+
data-testid="connect-success"
|
|
162
|
+
>
|
|
163
|
+
✓ Connected!
|
|
164
|
+
</div>
|
|
165
|
+
) : (
|
|
166
|
+
<Button
|
|
167
|
+
onClick={handleConnect}
|
|
168
|
+
disabled={state === 'waiting'}
|
|
169
|
+
size="lg"
|
|
170
|
+
fullWidth
|
|
171
|
+
data-testid="connect-claude-button"
|
|
172
|
+
>
|
|
173
|
+
{state === 'waiting' ? (
|
|
174
|
+
<span className="flex items-center justify-center gap-3">
|
|
175
|
+
<span className="inline-block w-4 h-4 border-2 border-[#c8d9da] border-t-[#3d4d4e] rounded-full animate-spin" />
|
|
176
|
+
Waiting for login...
|
|
177
|
+
</span>
|
|
178
|
+
) : state === 'error' ? (
|
|
179
|
+
'Try Again'
|
|
180
|
+
) : (
|
|
181
|
+
'Connect Claude Code'
|
|
182
|
+
)}
|
|
183
|
+
</Button>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* Status text */}
|
|
188
|
+
{state === 'waiting' && (
|
|
189
|
+
<p className="text-base text-zinc-400 dark:text-zinc-500 text-center">
|
|
190
|
+
A browser window should have opened. Complete the sign-in there.
|
|
191
|
+
</p>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{/* Info Section */}
|
|
195
|
+
<div className="pt-10 space-y-4">
|
|
196
|
+
<div className="border-2 border-zinc-200 dark:border-zinc-700 rounded-xl p-6 text-zinc-500 dark:text-zinc-400 text-base">
|
|
197
|
+
<p>
|
|
198
|
+
<strong className="text-zinc-700 dark:text-zinc-300">Why do I need this?</strong>
|
|
199
|
+
</p>
|
|
200
|
+
<p className="mt-2">
|
|
201
|
+
JettyPod uses Claude Code under the hood. Claude Code requires its own
|
|
202
|
+
Anthropic account to work. This is a one-time setup — you won't need to
|
|
203
|
+
do this again on this computer.
|
|
204
|
+
</p>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
@@ -51,15 +51,15 @@ export function CopyableId({ id, title, type, size = 'sm' }: CopyableIdProps) {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
const sizeClasses = size === 'md'
|
|
54
|
-
? 'text-
|
|
55
|
-
: 'text-
|
|
54
|
+
? 'text-base px-1.5 py-1'
|
|
55
|
+
: 'text-sm px-1 py-0.5';
|
|
56
56
|
|
|
57
57
|
const iconClasses = size === 'md' ? 'w-4 h-4' : 'w-3 h-3';
|
|
58
58
|
|
|
59
59
|
return (
|
|
60
60
|
<button
|
|
61
61
|
onClick={handleCopy}
|
|
62
|
-
className={`flex items-center gap-1 text-zinc-400 font-mono -mx-1 rounded cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-700 active:scale-95 transition-
|
|
62
|
+
className={`flex items-center gap-1 text-zinc-400 font-mono -mx-1 rounded cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-700 active:scale-95 transition-[color,background-color,transform] duration-200 ease-out ${sizeClasses}`}
|
|
63
63
|
title={`Copy: #${id} ${title} (${type})`}
|
|
64
64
|
>
|
|
65
65
|
<span>#{id}</span>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { Button } from '@/components/ui/Button';
|
|
6
|
+
import { Input } from '@/components/ui/Input';
|
|
7
|
+
|
|
8
|
+
interface DetailReviewActionsProps {
|
|
9
|
+
workItemId: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function DetailReviewActions({ workItemId }: DetailReviewActionsProps) {
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
const [showRejectInput, setShowRejectInput] = useState(false);
|
|
15
|
+
const [rejectReason, setRejectReason] = useState('');
|
|
16
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
17
|
+
|
|
18
|
+
const handleAccept = async () => {
|
|
19
|
+
setIsSubmitting(true);
|
|
20
|
+
const res = await fetch(`/api/work/${workItemId}/status`, {
|
|
21
|
+
method: 'PATCH',
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
body: JSON.stringify({ status: 'done' }),
|
|
24
|
+
});
|
|
25
|
+
if (res.ok) {
|
|
26
|
+
router.push('/');
|
|
27
|
+
} else {
|
|
28
|
+
setIsSubmitting(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleRejectConfirm = async () => {
|
|
33
|
+
if (!rejectReason.trim()) return;
|
|
34
|
+
setIsSubmitting(true);
|
|
35
|
+
const res = await fetch(`/api/work/${workItemId}/status`, {
|
|
36
|
+
method: 'PATCH',
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
body: JSON.stringify({ status: 'in_progress', rejectionReason: rejectReason.trim() }),
|
|
39
|
+
});
|
|
40
|
+
if (res.ok) {
|
|
41
|
+
router.push(`/?rejected=${workItemId}&reason=${encodeURIComponent(rejectReason.trim())}`);
|
|
42
|
+
} else {
|
|
43
|
+
setIsSubmitting(false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (showRejectInput) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex items-center gap-2" data-testid="detail-review-reject-area">
|
|
50
|
+
<Input
|
|
51
|
+
type="text"
|
|
52
|
+
value={rejectReason}
|
|
53
|
+
onChange={(e) => setRejectReason(e.target.value)}
|
|
54
|
+
onKeyDown={(e) => {
|
|
55
|
+
if (e.key === 'Enter' && rejectReason.trim()) handleRejectConfirm();
|
|
56
|
+
if (e.key === 'Escape') {
|
|
57
|
+
setShowRejectInput(false);
|
|
58
|
+
setRejectReason('');
|
|
59
|
+
}
|
|
60
|
+
}}
|
|
61
|
+
placeholder="Rejection reason..."
|
|
62
|
+
size="sm"
|
|
63
|
+
error
|
|
64
|
+
autoFocus
|
|
65
|
+
data-testid="detail-review-reject-input"
|
|
66
|
+
/>
|
|
67
|
+
<Button
|
|
68
|
+
onClick={handleRejectConfirm}
|
|
69
|
+
disabled={!rejectReason.trim()}
|
|
70
|
+
loading={isSubmitting}
|
|
71
|
+
variant="destructive"
|
|
72
|
+
size="sm"
|
|
73
|
+
data-testid="detail-review-reject-confirm"
|
|
74
|
+
>
|
|
75
|
+
Reject
|
|
76
|
+
</Button>
|
|
77
|
+
<Button
|
|
78
|
+
onClick={() => { setShowRejectInput(false); setRejectReason(''); }}
|
|
79
|
+
variant="ghost"
|
|
80
|
+
size="sm"
|
|
81
|
+
data-testid="detail-review-reject-cancel"
|
|
82
|
+
>
|
|
83
|
+
Cancel
|
|
84
|
+
</Button>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="flex items-center gap-2" data-testid="detail-review-actions">
|
|
91
|
+
<Button
|
|
92
|
+
onClick={handleAccept}
|
|
93
|
+
loading={isSubmitting}
|
|
94
|
+
size="sm"
|
|
95
|
+
data-testid="detail-review-accept"
|
|
96
|
+
>
|
|
97
|
+
Accept
|
|
98
|
+
</Button>
|
|
99
|
+
<Button
|
|
100
|
+
onClick={() => setShowRejectInput(true)}
|
|
101
|
+
variant="secondary"
|
|
102
|
+
size="sm"
|
|
103
|
+
data-testid="detail-review-reject"
|
|
104
|
+
>
|
|
105
|
+
Reject
|
|
106
|
+
</Button>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|