jettypod 4.4.115 → 4.4.118
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 +25 -9
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +7 -3
- 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/connect-claude/page.tsx +24 -0
- package/apps/dashboard/app/install-claude/page.tsx +8 -6
- package/apps/dashboard/app/login/page.tsx +229 -0
- package/apps/dashboard/app/page.tsx +5 -3
- package/apps/dashboard/app/settings/page.tsx +2 -0
- package/apps/dashboard/app/subscribe/page.tsx +11 -0
- package/apps/dashboard/app/welcome/page.tsx +23 -0
- package/apps/dashboard/components/AppShell.tsx +51 -9
- package/apps/dashboard/components/CardMenu.tsx +14 -5
- package/apps/dashboard/components/ClaudePanel.tsx +65 -9
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +223 -0
- package/apps/dashboard/components/DragContext.tsx +73 -64
- package/apps/dashboard/components/DraggableCard.tsx +6 -46
- package/apps/dashboard/components/GateCard.tsx +21 -0
- package/apps/dashboard/components/InstallClaudeScreen.tsx +132 -30
- package/apps/dashboard/components/KanbanBoard.tsx +173 -56
- package/apps/dashboard/components/PlaceholderCard.tsx +9 -19
- package/apps/dashboard/components/ProjectSwitcher.tsx +28 -0
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +34 -3
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +30 -2
- package/apps/dashboard/components/SubscribeContent.tsx +191 -0
- package/apps/dashboard/components/TipCard.tsx +176 -0
- package/apps/dashboard/components/UpgradeBanner.tsx +29 -0
- package/apps/dashboard/components/WelcomeScreen.tsx +14 -4
- package/apps/dashboard/components/settings/AccountSection.tsx +163 -0
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +292 -29
- package/apps/dashboard/contexts/UsageContext.tsx +131 -0
- package/apps/dashboard/contexts/usageHelpers.js +9 -0
- package/apps/dashboard/electron/ipc-handlers.js +220 -114
- package/apps/dashboard/electron/main.js +415 -37
- package/apps/dashboard/electron/preload.js +23 -4
- package/apps/dashboard/electron/session-manager.js +141 -0
- package/apps/dashboard/electron-builder.config.js +3 -5
- package/apps/dashboard/lib/claude-process-manager.ts +6 -4
- package/apps/dashboard/lib/db-bridge.ts +32 -0
- package/apps/dashboard/lib/db.ts +159 -13
- package/apps/dashboard/lib/session-state-machine.ts +3 -0
- package/apps/dashboard/lib/session-stream-manager.ts +76 -13
- package/apps/dashboard/lib/tests.ts +3 -1
- package/apps/dashboard/next.config.js +19 -14
- package/apps/dashboard/package.json +3 -1
- package/apps/dashboard/scripts/upload-to-r2.js +89 -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 +1074 -0
- package/apps/update-server/tsconfig.json +16 -0
- package/apps/update-server/wrangler.toml +35 -0
- package/docs/bdd-guidance.md +390 -0
- package/jettypod.js +5 -4
- package/lib/migrations/027-plan-at-creation-column.js +31 -0
- package/lib/migrations/028-ready-for-review-column.js +27 -0
- package/lib/schema.js +3 -1
- package/lib/seed-onboarding.js +100 -68
- package/lib/work-commands/index.js +43 -13
- package/lib/work-tracking/index.js +46 -27
- package/package.json +1 -1
- package/skills-templates/bug-mode/SKILL.md +5 -11
- package/skills-templates/request-routing/SKILL.md +24 -11
- package/skills-templates/simple-improvement/SKILL.md +35 -19
- package/skills-templates/stable-mode/SKILL.md +5 -6
- 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
|
@@ -7,14 +7,17 @@ interface CardMenuProps {
|
|
|
7
7
|
itemId: number;
|
|
8
8
|
itemTitle?: string;
|
|
9
9
|
itemType?: string;
|
|
10
|
+
itemDescription?: string | null;
|
|
11
|
+
conversational?: boolean;
|
|
10
12
|
currentStatus: string;
|
|
11
13
|
onStatusChange: (id: number, newStatus: string) => Promise<void>;
|
|
12
|
-
onTriggerClaude?: (id: number, title: string, type: string) => void;
|
|
14
|
+
onTriggerClaude?: (id: number, title: string, type: string, conversational?: boolean, description?: string | null) => void;
|
|
13
15
|
hasActiveSession?: boolean;
|
|
14
16
|
onOpenSession?: (id: string) => void;
|
|
17
|
+
usageAllowed?: boolean;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentStatus, onStatusChange, onTriggerClaude, hasActiveSession, onOpenSession }: CardMenuProps) {
|
|
20
|
+
export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', itemDescription, conversational = false, currentStatus, onStatusChange, onTriggerClaude, hasActiveSession, onOpenSession, usageAllowed = true }: CardMenuProps) {
|
|
18
21
|
const [isOpen, setIsOpen] = useState(false);
|
|
19
22
|
const [error, setError] = useState<string | null>(null);
|
|
20
23
|
const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number } | null>(null);
|
|
@@ -74,7 +77,7 @@ export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentSt
|
|
|
74
77
|
await onStatusChange(itemId, 'in_progress');
|
|
75
78
|
// Trigger Claude panel after status change succeeds
|
|
76
79
|
if (onTriggerClaude) {
|
|
77
|
-
onTriggerClaude(itemId, itemTitle, itemType);
|
|
80
|
+
onTriggerClaude(itemId, itemTitle, itemType, conversational, itemDescription);
|
|
78
81
|
}
|
|
79
82
|
} catch (err) {
|
|
80
83
|
setError(err instanceof Error ? err.message : 'Failed to update status');
|
|
@@ -102,10 +105,16 @@ export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentSt
|
|
|
102
105
|
{(currentStatus === 'backlog' || currentStatus === 'cancelled') && (
|
|
103
106
|
<button
|
|
104
107
|
onClick={handleStart}
|
|
105
|
-
|
|
108
|
+
disabled={!usageAllowed}
|
|
109
|
+
className={`w-full px-3 py-2 text-left text-sm flex items-center gap-2 ${
|
|
110
|
+
usageAllowed
|
|
111
|
+
? 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700'
|
|
112
|
+
: 'text-zinc-400 dark:text-zinc-600 cursor-not-allowed'
|
|
113
|
+
}`}
|
|
106
114
|
data-testid="start-button"
|
|
115
|
+
title={!usageAllowed ? 'Weekly usage limit reached' : undefined}
|
|
107
116
|
>
|
|
108
|
-
<span className=
|
|
117
|
+
<span className={usageAllowed ? 'text-blue-500' : 'text-zinc-400 dark:text-zinc-600'}>▶</span>
|
|
109
118
|
Start
|
|
110
119
|
</button>
|
|
111
120
|
)}
|
|
@@ -10,6 +10,39 @@ import { GateCard } from './GateCard';
|
|
|
10
10
|
import type { SessionItem } from './SessionList';
|
|
11
11
|
import type { Session } from '../contexts/ClaudeSessionContext';
|
|
12
12
|
|
|
13
|
+
// Tool name → human-friendly verb mapping for activity indicator
|
|
14
|
+
const TOOL_VERBS: Record<string, string> = {
|
|
15
|
+
Read: 'Reading',
|
|
16
|
+
Grep: 'Searching for',
|
|
17
|
+
Glob: 'Finding files matching',
|
|
18
|
+
Bash: 'Running',
|
|
19
|
+
Edit: 'Editing',
|
|
20
|
+
Write: 'Writing',
|
|
21
|
+
Task: 'Delegating',
|
|
22
|
+
WebFetch: 'Fetching',
|
|
23
|
+
WebSearch: 'Searching web for',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function extractFilename(path: string): string {
|
|
27
|
+
return path.split('/').pop() || path;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function humanizeToolCall(toolName: string, param: string): string {
|
|
31
|
+
const verb = TOOL_VERBS[toolName] || toolName;
|
|
32
|
+
if (['Read', 'Edit', 'Write'].includes(toolName)) {
|
|
33
|
+
return `${verb} ${extractFilename(param)}...`;
|
|
34
|
+
}
|
|
35
|
+
if (toolName === 'Bash') {
|
|
36
|
+
const short = param.length > 40 ? param.slice(0, 40) : param;
|
|
37
|
+
return `${verb} ${short}...`;
|
|
38
|
+
}
|
|
39
|
+
if (['Grep', 'Glob', 'WebSearch'].includes(toolName)) {
|
|
40
|
+
const short = param.length > 30 ? param.slice(0, 30) : param;
|
|
41
|
+
return `${verb} ${short}...`;
|
|
42
|
+
}
|
|
43
|
+
return `${verb} ${param}...`;
|
|
44
|
+
}
|
|
45
|
+
|
|
13
46
|
// Unescape content that may have literal \n, \t, \r from JSON stringification
|
|
14
47
|
function unescapeContent(content: string | undefined): string {
|
|
15
48
|
if (!content) return '';
|
|
@@ -187,6 +220,7 @@ interface ClaudePanelProps {
|
|
|
187
220
|
error: string | null;
|
|
188
221
|
exitCode: number | null;
|
|
189
222
|
canRetry: boolean;
|
|
223
|
+
queuedMessage?: { message: string; images?: unknown[] } | null;
|
|
190
224
|
onClose: () => void;
|
|
191
225
|
onRetry: () => void;
|
|
192
226
|
onSendMessage: (message: string, images?: Array<{ type: string; dataUrl: string }>) => void;
|
|
@@ -213,6 +247,7 @@ export function ClaudePanel({
|
|
|
213
247
|
error,
|
|
214
248
|
exitCode,
|
|
215
249
|
canRetry,
|
|
250
|
+
queuedMessage,
|
|
216
251
|
onClose,
|
|
217
252
|
onRetry,
|
|
218
253
|
onSendMessage,
|
|
@@ -228,6 +263,9 @@ export function ClaudePanel({
|
|
|
228
263
|
}: ClaudePanelProps) {
|
|
229
264
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
230
265
|
const hasGates = messages.some(m => m.type === 'gate');
|
|
266
|
+
// Force detail view when no user messages (e.g., welcome session with static content)
|
|
267
|
+
const hasUserMessages = messages.some(m => m.type === 'user');
|
|
268
|
+
const effectiveNarratedMode = hasUserMessages ? narratedMode : false;
|
|
231
269
|
|
|
232
270
|
// Track answered question gates by timestamp → selected option id
|
|
233
271
|
const [answeredQuestions, setAnsweredQuestions] = useState<Map<number, string>>(new Map());
|
|
@@ -247,7 +285,7 @@ export function ClaudePanel({
|
|
|
247
285
|
// Reset expanded state when toggling between summary/detail views
|
|
248
286
|
useEffect(() => {
|
|
249
287
|
setExpandedIndices(new Set());
|
|
250
|
-
}, [
|
|
288
|
+
}, [effectiveNarratedMode]);
|
|
251
289
|
|
|
252
290
|
// Drag-and-drop state lifted to panel level so the entire panel is a drop target
|
|
253
291
|
const [isDragging, setIsDragging] = useState(false);
|
|
@@ -367,14 +405,14 @@ export function ClaudePanel({
|
|
|
367
405
|
<button
|
|
368
406
|
onClick={onToggleNarratedMode}
|
|
369
407
|
className={`px-2 py-1 rounded text-xs font-medium transition-colors ${
|
|
370
|
-
|
|
408
|
+
effectiveNarratedMode
|
|
371
409
|
? 'bg-blue-100 text-blue-700 hover:bg-blue-200'
|
|
372
410
|
: 'bg-zinc-100 text-zinc-500 hover:bg-zinc-200'
|
|
373
411
|
}`}
|
|
374
|
-
aria-label={
|
|
412
|
+
aria-label={effectiveNarratedMode ? 'Show full conversation' : 'Show summary view'}
|
|
375
413
|
data-testid="narrated-mode-toggle"
|
|
376
414
|
>
|
|
377
|
-
{
|
|
415
|
+
{effectiveNarratedMode ? 'Summary' : 'Details'}
|
|
378
416
|
</button>
|
|
379
417
|
)}
|
|
380
418
|
<button
|
|
@@ -540,7 +578,7 @@ export function ClaudePanel({
|
|
|
540
578
|
</div>
|
|
541
579
|
) : (
|
|
542
580
|
<>
|
|
543
|
-
{
|
|
581
|
+
{effectiveNarratedMode ? (
|
|
544
582
|
/* Narrated mode: show gate cards, user messages, assistant text, and a working indicator */
|
|
545
583
|
<>
|
|
546
584
|
{(() => {
|
|
@@ -591,8 +629,14 @@ export function ClaudePanel({
|
|
|
591
629
|
{status === 'streaming' && (
|
|
592
630
|
<div className="flex items-center gap-2 text-xs text-zinc-400 py-2">
|
|
593
631
|
<span className="w-1.5 h-1.5 bg-blue-400 rounded-full animate-pulse" />
|
|
594
|
-
Working...
|
|
595
632
|
<ElapsedTimer isStreaming={true} timerKey={`${activeSessionId ?? 'default'}-${messages.filter(m => m.type === 'user').length}`} />
|
|
633
|
+
{(() => {
|
|
634
|
+
const lastToolUse = [...messages].reverse().find(m => m.type === 'tool_use');
|
|
635
|
+
if (!lastToolUse || !lastToolUse.tool_name) return 'Working...';
|
|
636
|
+
const firstParamValue = lastToolUse.tool_input ? Object.values(lastToolUse.tool_input)[0] : null;
|
|
637
|
+
const param = typeof firstParamValue === 'string' ? firstParamValue : '';
|
|
638
|
+
return humanizeToolCall(lastToolUse.tool_name, param);
|
|
639
|
+
})()}
|
|
596
640
|
</div>
|
|
597
641
|
)}
|
|
598
642
|
</>
|
|
@@ -604,7 +648,8 @@ export function ClaudePanel({
|
|
|
604
648
|
const lastUserMessageIndex = filteredMessages.findLastIndex(m => m.type === 'user');
|
|
605
649
|
const userMessageCount = filteredMessages.filter(m => m.type === 'user').length;
|
|
606
650
|
const lastAssistantIndex = filteredMessages.findLastIndex(m => m.type === 'assistant' || m.type === 'text');
|
|
607
|
-
|
|
651
|
+
// Only show collapse UI when there's an actual conversation (user messages exist)
|
|
652
|
+
const hasIntermediates = userMessageCount > 0 && filteredMessages.filter(m => m.type === 'assistant' || m.type === 'text').length > 1;
|
|
608
653
|
|
|
609
654
|
const allExpanded = hasIntermediates && filteredMessages.every((m, i) =>
|
|
610
655
|
(m.type !== 'assistant' && m.type !== 'text') || i === lastAssistantIndex || expandedIndices.has(i)
|
|
@@ -638,7 +683,8 @@ export function ClaudePanel({
|
|
|
638
683
|
{filteredMessages.map((message, index) => {
|
|
639
684
|
const isAssistant = message.type === 'assistant' || message.type === 'text';
|
|
640
685
|
const isFinal = isAssistant && index === lastAssistantIndex;
|
|
641
|
-
|
|
686
|
+
// Don't collapse when no user messages (e.g., welcome session with static content)
|
|
687
|
+
const isIntermediate = isAssistant && !isFinal && userMessageCount > 0;
|
|
642
688
|
const isExpanded = expandedIndices.has(index);
|
|
643
689
|
|
|
644
690
|
// Get first line for collapsed summary
|
|
@@ -744,6 +790,16 @@ export function ClaudePanel({
|
|
|
744
790
|
What's next?
|
|
745
791
|
</div>
|
|
746
792
|
)}
|
|
793
|
+
{/* Queued message — shown below status indicator while Claude is processing */}
|
|
794
|
+
{queuedMessage && (
|
|
795
|
+
<div className="bg-blue-50 border border-blue-100 rounded-lg p-3 opacity-60" data-testid="queued-message">
|
|
796
|
+
<div className="flex items-center gap-1.5 mb-1">
|
|
797
|
+
<UserIcon />
|
|
798
|
+
<span className="text-xs text-zinc-400">Queued</span>
|
|
799
|
+
</div>
|
|
800
|
+
<div className="text-sm text-zinc-700 whitespace-pre-wrap">{queuedMessage.message}</div>
|
|
801
|
+
</div>
|
|
802
|
+
)}
|
|
747
803
|
</>
|
|
748
804
|
)}
|
|
749
805
|
</div>
|
|
@@ -974,7 +1030,7 @@ function ElapsedTimer({ isStreaming, timerKey }: { isStreaming: boolean; timerKe
|
|
|
974
1030
|
const display = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
|
975
1031
|
|
|
976
1032
|
return (
|
|
977
|
-
<div className="text-xs text-zinc-500
|
|
1033
|
+
<div className="text-xs text-zinc-500 -translate-y-px" data-testid="elapsed-timer">
|
|
978
1034
|
{display}
|
|
979
1035
|
</div>
|
|
980
1036
|
);
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
|
|
6
|
+
type ConnectState = 'idle' | 'waiting' | 'success' | 'error';
|
|
7
|
+
|
|
8
|
+
const AUTH_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
|
|
9
|
+
|
|
10
|
+
interface ConnectClaudeScreenProps {
|
|
11
|
+
onConnect: () => Promise<{ success: boolean; error?: string }>;
|
|
12
|
+
onCheckAuth: () => Promise<boolean>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ConnectClaudeScreen({ onConnect, onCheckAuth }: ConnectClaudeScreenProps) {
|
|
16
|
+
const [state, setState] = useState<ConnectState>('idle');
|
|
17
|
+
const [completedSteps, setCompletedSteps] = useState<number[]>([]);
|
|
18
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
19
|
+
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
20
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
21
|
+
|
|
22
|
+
const cleanup = () => {
|
|
23
|
+
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
|
|
24
|
+
if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
return cleanup;
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const handleSuccess = () => {
|
|
32
|
+
cleanup();
|
|
33
|
+
setCompletedSteps([1, 2, 3]);
|
|
34
|
+
setState('success');
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
window.location.href = '/';
|
|
37
|
+
}, 1500);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleError = (message: string) => {
|
|
41
|
+
cleanup();
|
|
42
|
+
setCompletedSteps([]);
|
|
43
|
+
setErrorMessage(message);
|
|
44
|
+
setState('error');
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handleConnect = async () => {
|
|
48
|
+
setState('waiting');
|
|
49
|
+
setCompletedSteps([1]);
|
|
50
|
+
setErrorMessage(null);
|
|
51
|
+
|
|
52
|
+
// Trigger claude login (opens browser)
|
|
53
|
+
let result: { success: boolean; error?: string };
|
|
54
|
+
try {
|
|
55
|
+
result = await onConnect();
|
|
56
|
+
} catch {
|
|
57
|
+
handleError('Failed to start Claude login. Please try again.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (result.success) {
|
|
62
|
+
handleSuccess();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Start polling in case login succeeds via browser after process exits
|
|
67
|
+
pollRef.current = setInterval(async () => {
|
|
68
|
+
try {
|
|
69
|
+
const authed = await onCheckAuth();
|
|
70
|
+
if (authed) {
|
|
71
|
+
handleSuccess();
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Polling check failed — ignore and retry on next interval
|
|
75
|
+
}
|
|
76
|
+
}, 2000);
|
|
77
|
+
|
|
78
|
+
// Set timeout to stop polling after AUTH_TIMEOUT_MS
|
|
79
|
+
timeoutRef.current = setTimeout(() => {
|
|
80
|
+
handleError('Authentication timed out. Make sure you completed sign-in in the browser, then try again.');
|
|
81
|
+
}, AUTH_TIMEOUT_MS);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const steps = [
|
|
85
|
+
'Open Anthropic login in browser',
|
|
86
|
+
'Sign in to your Anthropic account',
|
|
87
|
+
'Return to JettyPod — ready to go',
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900 p-8">
|
|
92
|
+
<div className="max-w-md w-full space-y-8">
|
|
93
|
+
{/* Logo */}
|
|
94
|
+
<div className="flex flex-col items-center space-y-4">
|
|
95
|
+
<Image
|
|
96
|
+
src="/jettypod_wordmark.png"
|
|
97
|
+
alt="JettyPod"
|
|
98
|
+
width={160}
|
|
99
|
+
height={40}
|
|
100
|
+
priority
|
|
101
|
+
/>
|
|
102
|
+
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
|
|
103
|
+
Connect Claude Code
|
|
104
|
+
</h1>
|
|
105
|
+
<p className="text-zinc-500 dark:text-zinc-400 text-center">
|
|
106
|
+
Claude Code needs to be connected to your Anthropic account.
|
|
107
|
+
This will open your browser to sign in.
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Error Banner */}
|
|
112
|
+
{errorMessage && (
|
|
113
|
+
<div
|
|
114
|
+
className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded-lg text-sm"
|
|
115
|
+
data-testid="connect-error"
|
|
116
|
+
>
|
|
117
|
+
{errorMessage}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{/* Progress Stepper */}
|
|
122
|
+
<div className="flex flex-col gap-4">
|
|
123
|
+
{steps.map((label, i) => {
|
|
124
|
+
const stepNum = i + 1;
|
|
125
|
+
const isDone = completedSteps.includes(stepNum);
|
|
126
|
+
const isActive = !isDone && (
|
|
127
|
+
(state === 'idle' && stepNum === 1) ||
|
|
128
|
+
(state === 'waiting' && stepNum === Math.max(...completedSteps) + 1)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div
|
|
133
|
+
key={stepNum}
|
|
134
|
+
className={`flex items-center gap-3 text-sm ${
|
|
135
|
+
isDone ? 'text-zinc-900 dark:text-zinc-100' :
|
|
136
|
+
isActive ? 'text-zinc-900 dark:text-zinc-100 font-medium' :
|
|
137
|
+
'text-zinc-400 dark:text-zinc-500'
|
|
138
|
+
}`}
|
|
139
|
+
>
|
|
140
|
+
<span
|
|
141
|
+
className={`w-7 h-7 rounded-full flex items-center justify-center text-xs font-semibold flex-shrink-0 transition-all duration-300 ${
|
|
142
|
+
isDone ? 'bg-green-400 text-white' :
|
|
143
|
+
isActive ? 'bg-[#c8d9da] text-[#3d4d4e]' :
|
|
144
|
+
'bg-zinc-200 dark:bg-zinc-700 text-zinc-400 dark:text-zinc-500'
|
|
145
|
+
}`}
|
|
146
|
+
>
|
|
147
|
+
{isDone ? '✓' : stepNum}
|
|
148
|
+
</span>
|
|
149
|
+
<span>{label}</span>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
})}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Connect Button */}
|
|
156
|
+
<div className="pt-4">
|
|
157
|
+
{state === 'success' ? (
|
|
158
|
+
<div
|
|
159
|
+
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"
|
|
160
|
+
data-testid="connect-success"
|
|
161
|
+
>
|
|
162
|
+
✓ Connected!
|
|
163
|
+
</div>
|
|
164
|
+
) : (
|
|
165
|
+
<button
|
|
166
|
+
onClick={handleConnect}
|
|
167
|
+
disabled={state === 'waiting'}
|
|
168
|
+
className="w-full py-3 px-6 rounded-xl font-medium transition-all duration-200 hover:-translate-y-1 hover:scale-[1.01] active:translate-y-0 active:scale-100 disabled:opacity-60 disabled:transform-none disabled:cursor-default"
|
|
169
|
+
style={{
|
|
170
|
+
cursor: state === 'waiting' ? 'default' : 'pointer',
|
|
171
|
+
background: 'linear-gradient(145deg, #ffffff 0%, #faf9f7 10%, #f0f4f4 35%, #c8d9da 55%, #819D9F 90%)',
|
|
172
|
+
color: '#3d4d4e',
|
|
173
|
+
boxShadow: `
|
|
174
|
+
0 1px 1px rgba(0, 0, 0, 0.02),
|
|
175
|
+
0 2px 4px rgba(0, 0, 0, 0.03),
|
|
176
|
+
0 6px 12px rgba(0, 0, 0, 0.05),
|
|
177
|
+
0 12px 24px rgba(0, 0, 0, 0.06),
|
|
178
|
+
0 20px 40px rgba(129, 157, 159, 0.2),
|
|
179
|
+
0 32px 64px rgba(129, 157, 159, 0.18),
|
|
180
|
+
inset 0 2px 4px rgba(255, 255, 255, 1),
|
|
181
|
+
inset 0 -2px 4px rgba(129, 157, 159, 0.05)
|
|
182
|
+
`,
|
|
183
|
+
}}
|
|
184
|
+
data-testid="connect-claude-button"
|
|
185
|
+
>
|
|
186
|
+
{state === 'waiting' ? (
|
|
187
|
+
<span className="flex items-center justify-center gap-2">
|
|
188
|
+
<span className="inline-block w-4 h-4 border-2 border-[#c8d9da] border-t-[#3d4d4e] rounded-full animate-spin" />
|
|
189
|
+
Waiting for login...
|
|
190
|
+
</span>
|
|
191
|
+
) : state === 'error' ? (
|
|
192
|
+
'Try Again'
|
|
193
|
+
) : (
|
|
194
|
+
'Connect Claude Code'
|
|
195
|
+
)}
|
|
196
|
+
</button>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Status text */}
|
|
201
|
+
{state === 'waiting' && (
|
|
202
|
+
<p className="text-sm text-zinc-400 dark:text-zinc-500 text-center">
|
|
203
|
+
A browser window should have opened. Complete the sign-in there.
|
|
204
|
+
</p>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
{/* Info Section */}
|
|
208
|
+
<div className="pt-8 space-y-4">
|
|
209
|
+
<div className="border border-zinc-200 dark:border-zinc-700 rounded-lg p-4 text-zinc-500 dark:text-zinc-400 text-sm">
|
|
210
|
+
<p>
|
|
211
|
+
<strong className="text-zinc-700 dark:text-zinc-300">Why do I need this?</strong>
|
|
212
|
+
</p>
|
|
213
|
+
<p className="mt-2">
|
|
214
|
+
JettyPod uses Claude Code under the hood. Claude Code requires its own
|
|
215
|
+
Anthropic account to work. This is a one-time setup — you won't need to
|
|
216
|
+
do this again on this computer.
|
|
217
|
+
</p>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|