jettypod 4.4.116 → 4.4.120

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/.env +7 -0
  2. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
  3. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
  4. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
  5. package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
  6. package/apps/dashboard/app/api/usage/route.ts +17 -0
  7. package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
  8. package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
  9. package/apps/dashboard/app/connect-claude/page.tsx +24 -0
  10. package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
  11. package/apps/dashboard/app/demo/gates/page.tsx +42 -42
  12. package/apps/dashboard/app/design-system/page.tsx +868 -0
  13. package/apps/dashboard/app/globals.css +6 -2
  14. package/apps/dashboard/app/install-claude/page.tsx +9 -7
  15. package/apps/dashboard/app/layout.tsx +17 -5
  16. package/apps/dashboard/app/login/page.tsx +250 -0
  17. package/apps/dashboard/app/page.tsx +11 -9
  18. package/apps/dashboard/app/settings/page.tsx +4 -2
  19. package/apps/dashboard/app/signup/page.tsx +245 -0
  20. package/apps/dashboard/app/subscribe/page.tsx +11 -0
  21. package/apps/dashboard/app/welcome/page.tsx +24 -1
  22. package/apps/dashboard/app/work/[id]/page.tsx +34 -50
  23. package/apps/dashboard/components/AppShell.tsx +95 -55
  24. package/apps/dashboard/components/CardMenu.tsx +56 -13
  25. package/apps/dashboard/components/ClaudePanel.tsx +301 -582
  26. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
  27. package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
  28. package/apps/dashboard/components/CopyableId.tsx +3 -3
  29. package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
  30. package/apps/dashboard/components/DragContext.tsx +75 -65
  31. package/apps/dashboard/components/DraggableCard.tsx +6 -46
  32. package/apps/dashboard/components/DropZone.tsx +2 -2
  33. package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
  34. package/apps/dashboard/components/EditableTitle.tsx +26 -6
  35. package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
  36. package/apps/dashboard/components/EpicGroup.tsx +329 -0
  37. package/apps/dashboard/components/GateCard.tsx +100 -16
  38. package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
  39. package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
  40. package/apps/dashboard/components/JettyLoader.tsx +38 -0
  41. package/apps/dashboard/components/KanbanBoard.tsx +147 -766
  42. package/apps/dashboard/components/KanbanCard.tsx +506 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
  44. package/apps/dashboard/components/MainNav.tsx +20 -54
  45. package/apps/dashboard/components/MessageBlock.tsx +391 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -15
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
  53. package/apps/dashboard/components/ReviewFooter.tsx +141 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -18
  55. package/apps/dashboard/components/SubscribeContent.tsx +206 -0
  56. package/apps/dashboard/components/TestTree.tsx +15 -14
  57. package/apps/dashboard/components/TipCard.tsx +177 -0
  58. package/apps/dashboard/components/Toast.tsx +5 -5
  59. package/apps/dashboard/components/TypeIcon.tsx +56 -0
  60. package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
  62. package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
  64. package/apps/dashboard/components/WorkItemTree.tsx +9 -28
  65. package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
  66. package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
  67. package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
  68. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
  69. package/apps/dashboard/components/ui/Button.tsx +104 -0
  70. package/apps/dashboard/components/ui/Input.tsx +78 -0
  71. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
  72. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
  73. package/apps/dashboard/contexts/UsageContext.tsx +155 -0
  74. package/apps/dashboard/contexts/usageHelpers.js +9 -0
  75. package/apps/dashboard/electron/ipc-handlers.js +281 -88
  76. package/apps/dashboard/electron/main.js +691 -131
  77. package/apps/dashboard/electron/preload.js +25 -4
  78. package/apps/dashboard/electron/session-manager.js +163 -0
  79. package/apps/dashboard/electron-builder.config.js +3 -5
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/lib/backlog-parser.ts +50 -0
  83. package/apps/dashboard/lib/claude-process-manager.ts +50 -11
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/db-bridge.ts +33 -0
  86. package/apps/dashboard/lib/db.ts +136 -20
  87. package/apps/dashboard/lib/kanban-utils.ts +70 -0
  88. package/apps/dashboard/lib/run-migrations.js +27 -2
  89. package/apps/dashboard/lib/session-state-machine.ts +3 -0
  90. package/apps/dashboard/lib/session-stream-manager.ts +144 -38
  91. package/apps/dashboard/lib/shadows.ts +7 -0
  92. package/apps/dashboard/lib/tests.ts +3 -1
  93. package/apps/dashboard/lib/utils.ts +6 -0
  94. package/apps/dashboard/next.config.js +35 -14
  95. package/apps/dashboard/package.json +6 -3
  96. package/apps/dashboard/public/bug-icon.svg +9 -0
  97. package/apps/dashboard/public/buoy-icon.svg +9 -0
  98. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  99. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  100. package/apps/dashboard/public/in-flight-seagull.svg +9 -0
  101. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  102. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  103. package/apps/dashboard/public/jettypod_logo.png +0 -0
  104. package/apps/dashboard/public/pier-icon.svg +14 -0
  105. package/apps/dashboard/public/star-icon.svg +9 -0
  106. package/apps/dashboard/public/wrench-icon.svg +9 -0
  107. package/apps/dashboard/scripts/upload-to-r2.js +89 -0
  108. package/apps/dashboard/scripts/ws-server.js +191 -0
  109. package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
  110. package/apps/update-server/package.json +16 -0
  111. package/apps/update-server/schema.sql +31 -0
  112. package/apps/update-server/src/index.ts +1085 -0
  113. package/apps/update-server/tsconfig.json +16 -0
  114. package/apps/update-server/wrangler.toml +35 -0
  115. package/cucumber.js +9 -3
  116. package/docs/COMMAND_REFERENCE.md +34 -0
  117. package/hooks/post-checkout +32 -75
  118. package/hooks/post-merge +111 -10
  119. package/jest.setup.js +1 -0
  120. package/jettypod.js +54 -116
  121. package/lib/chore-taxonomy.js +33 -10
  122. package/lib/database.js +36 -16
  123. package/lib/db-watcher.js +1 -1
  124. package/lib/git-hooks/pre-commit +1 -1
  125. package/lib/jettypod-backup.js +27 -4
  126. package/lib/migrations/027-plan-at-creation-column.js +33 -0
  127. package/lib/migrations/028-ready-for-review-column.js +27 -0
  128. package/lib/migrations/029-remove-autoincrement.js +307 -0
  129. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  130. package/lib/migrations/index.js +47 -4
  131. package/lib/schema.js +13 -6
  132. package/lib/seed-onboarding.js +101 -69
  133. package/lib/update-command/index.js +9 -175
  134. package/lib/work-commands/index.js +129 -16
  135. package/lib/work-tracking/index.js +86 -46
  136. package/lib/worktree-diagnostics.js +16 -16
  137. package/lib/worktree-facade.js +1 -1
  138. package/lib/worktree-manager.js +8 -8
  139. package/lib/worktree-reconciler.js +5 -5
  140. package/package.json +9 -2
  141. package/scripts/ndjson-to-cucumber-json.js +152 -0
  142. package/scripts/postinstall.js +25 -0
  143. package/skills-templates/bug-mode/SKILL.md +39 -28
  144. package/skills-templates/bug-planning/SKILL.md +25 -29
  145. package/skills-templates/chore-mode/SKILL.md +131 -68
  146. package/skills-templates/chore-mode/verification.js +51 -10
  147. package/skills-templates/chore-planning/SKILL.md +47 -18
  148. package/skills-templates/epic-planning/SKILL.md +68 -48
  149. package/skills-templates/external-transition/SKILL.md +47 -47
  150. package/skills-templates/feature-planning/SKILL.md +83 -73
  151. package/skills-templates/production-mode/SKILL.md +49 -49
  152. package/skills-templates/request-routing/SKILL.md +27 -14
  153. package/skills-templates/simple-improvement/SKILL.md +68 -44
  154. package/skills-templates/speed-mode/SKILL.md +209 -128
  155. package/skills-templates/stable-mode/SKILL.md +105 -94
  156. package/templates/bdd-guidance.md +139 -0
  157. package/templates/bdd-scaffolding/wait.js +18 -0
  158. package/templates/bdd-scaffolding/world.js +19 -0
  159. package/.jettypod-backup/work.db +0 -0
  160. package/apps/dashboard/app/access-code/page.tsx +0 -110
  161. package/lib/discovery-checkpoint.js +0 -123
  162. package/skills-templates/project-discovery/SKILL.md +0 -372
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { useState, useRef, useCallback, KeyboardEvent, ChangeEvent } from 'react';
4
- import { motion, AnimatePresence } from 'framer-motion';
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-3"
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-all duration-200
102
- ${isFocused ? 'border-blue-500 bg-white' : 'border-zinc-300 bg-white'}
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-2 px-3 pt-2" data-testid="image-preview-section">
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 border border-zinc-200"
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-3 py-2.5 text-sm text-zinc-900
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-all duration-200
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
- <motion.div
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-3 pb-2 text-xs text-zinc-500"
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 stop
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
- </motion.div>
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&apos;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-sm px-1.5 py-1'
55
- : 'text-xs px-1 py-0.5';
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-all ${sizeClasses}`}
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
+ }