jettypod 4.4.118 → 4.4.121

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 (240) hide show
  1. package/.env +4 -3
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
  8. package/apps/dashboard/app/demo/gates/page.tsx +43 -45
  9. package/apps/dashboard/app/design-system/page.tsx +868 -0
  10. package/apps/dashboard/app/globals.css +80 -4
  11. package/apps/dashboard/app/install-claude/page.tsx +4 -6
  12. package/apps/dashboard/app/login/page.tsx +72 -54
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +61 -13
  15. package/apps/dashboard/app/signup/page.tsx +242 -0
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +13 -16
  19. package/apps/dashboard/app/work/[id]/page.tsx +117 -118
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +92 -85
  22. package/apps/dashboard/components/CardMenu.tsx +45 -12
  23. package/apps/dashboard/components/ClaudePanel.tsx +771 -850
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
  26. package/apps/dashboard/components/CopyableId.tsx +3 -4
  27. package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
  28. package/apps/dashboard/components/DragContext.tsx +134 -63
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +6 -7
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +26 -7
  34. package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
  35. package/apps/dashboard/components/EpicGroup.tsx +359 -0
  36. package/apps/dashboard/components/GateCard.tsx +79 -17
  37. package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
  39. package/apps/dashboard/components/JettyLoader.tsx +37 -0
  40. package/apps/dashboard/components/KanbanBoard.tsx +368 -958
  41. package/apps/dashboard/components/KanbanCard.tsx +740 -0
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
  44. package/apps/dashboard/components/MainNav.tsx +38 -73
  45. package/apps/dashboard/components/MessageBlock.tsx +468 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -16
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
  53. package/apps/dashboard/components/ReviewFooter.tsx +139 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -19
  55. package/apps/dashboard/components/SubscribeContent.tsx +91 -47
  56. package/apps/dashboard/components/TestTree.tsx +16 -16
  57. package/apps/dashboard/components/TipCard.tsx +16 -17
  58. package/apps/dashboard/components/Toast.tsx +5 -6
  59. package/apps/dashboard/components/TypeIcon.tsx +55 -0
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
  62. package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
  64. package/apps/dashboard/components/WorkItemTree.tsx +11 -32
  65. package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
  72. package/apps/dashboard/components/ui/Button.tsx +104 -0
  73. package/apps/dashboard/components/ui/Input.tsx +78 -0
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
  77. package/apps/dashboard/contexts/UsageContext.tsx +87 -32
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  83. package/apps/dashboard/index.html +73 -0
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/data-bridge.ts +722 -0
  86. package/apps/dashboard/lib/db.ts +69 -1265
  87. package/apps/dashboard/lib/environment-config.ts +173 -0
  88. package/apps/dashboard/lib/environment-verification.ts +119 -0
  89. package/apps/dashboard/lib/kanban-utils.ts +270 -0
  90. package/apps/dashboard/lib/proof-run.ts +495 -0
  91. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  92. package/apps/dashboard/lib/run-migrations.js +27 -2
  93. package/apps/dashboard/lib/service-recovery.ts +326 -0
  94. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  95. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  96. package/apps/dashboard/lib/session-stream-manager.ts +308 -134
  97. package/apps/dashboard/lib/shadows.ts +7 -0
  98. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  99. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  100. package/apps/dashboard/lib/tauri.ts +106 -0
  101. package/apps/dashboard/lib/utils.ts +6 -0
  102. package/apps/dashboard/next-env.d.ts +1 -1
  103. package/apps/dashboard/package.json +21 -32
  104. package/apps/dashboard/public/bug-icon.png +0 -0
  105. package/apps/dashboard/public/buoy-icon.png +0 -0
  106. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  107. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  108. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  109. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  110. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  111. package/apps/dashboard/public/jettypod_logo.png +0 -0
  112. package/apps/dashboard/public/pier-icon.png +0 -0
  113. package/apps/dashboard/public/star-icon.png +0 -0
  114. package/apps/dashboard/public/wrench-icon.png +0 -0
  115. package/apps/dashboard/scripts/tauri-build.js +228 -0
  116. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  117. package/apps/dashboard/scripts/ws-server.js +191 -0
  118. package/apps/dashboard/src/main.tsx +12 -0
  119. package/apps/dashboard/src/router.tsx +107 -0
  120. package/apps/dashboard/src/vite-env.d.ts +1 -0
  121. package/apps/dashboard/tsconfig.json +7 -12
  122. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  123. package/apps/dashboard/vite.config.ts +33 -0
  124. package/apps/update-server/src/index.ts +228 -80
  125. package/claude-hooks/global-guardrails.js +14 -13
  126. package/crates/jettypod-cli/Cargo.toml +19 -0
  127. package/crates/jettypod-cli/src/commands.rs +1249 -0
  128. package/crates/jettypod-cli/src/main.rs +595 -0
  129. package/crates/jettypod-core/Cargo.toml +26 -0
  130. package/crates/jettypod-core/build.rs +98 -0
  131. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  132. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  133. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  134. package/crates/jettypod-core/src/auth.rs +294 -0
  135. package/crates/jettypod-core/src/config.rs +397 -0
  136. package/crates/jettypod-core/src/db/mod.rs +507 -0
  137. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  138. package/crates/jettypod-core/src/db/startup.rs +101 -0
  139. package/crates/jettypod-core/src/db/validate.rs +149 -0
  140. package/crates/jettypod-core/src/error.rs +76 -0
  141. package/crates/jettypod-core/src/git.rs +458 -0
  142. package/crates/jettypod-core/src/lib.rs +20 -0
  143. package/crates/jettypod-core/src/sessions.rs +625 -0
  144. package/crates/jettypod-core/src/skills.rs +556 -0
  145. package/crates/jettypod-core/src/work.rs +1086 -0
  146. package/crates/jettypod-core/src/worktree.rs +628 -0
  147. package/crates/jettypod-core/src/ws.rs +767 -0
  148. package/cucumber-test.cjs +6 -0
  149. package/cucumber.js +9 -3
  150. package/docs/COMMAND_REFERENCE.md +34 -0
  151. package/hooks/post-checkout +32 -75
  152. package/hooks/post-merge +111 -10
  153. package/jest.setup.js +1 -0
  154. package/jettypod.js +145 -116
  155. package/lib/bdd-preflight.js +96 -0
  156. package/lib/chore-taxonomy.js +33 -10
  157. package/lib/database.js +36 -16
  158. package/lib/db-watcher.js +1 -1
  159. package/lib/git-hooks/pre-commit +1 -1
  160. package/lib/jettypod-backup.js +27 -4
  161. package/lib/merge-lock.js +111 -253
  162. package/lib/migrations/027-plan-at-creation-column.js +3 -1
  163. package/lib/migrations/029-remove-autoincrement.js +307 -0
  164. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  165. package/lib/migrations/030-rejection-round-columns.js +54 -0
  166. package/lib/migrations/031-session-isolation-index.js +17 -0
  167. package/lib/migrations/index.js +47 -4
  168. package/lib/schema.js +10 -5
  169. package/lib/seed-onboarding.js +1 -1
  170. package/lib/update-command/index.js +9 -175
  171. package/lib/work-commands/index.js +144 -19
  172. package/lib/work-tracking/index.js +148 -27
  173. package/lib/worktree-diagnostics.js +16 -16
  174. package/lib/worktree-facade.js +1 -1
  175. package/lib/worktree-manager.js +8 -8
  176. package/lib/worktree-reconciler.js +5 -5
  177. package/package.json +9 -2
  178. package/scripts/ndjson-to-cucumber-json.js +152 -0
  179. package/scripts/postinstall.js +25 -0
  180. package/skills-templates/bug-mode/SKILL.md +79 -20
  181. package/skills-templates/bug-planning/SKILL.md +25 -29
  182. package/skills-templates/chore-mode/SKILL.md +171 -69
  183. package/skills-templates/chore-mode/verification.js +51 -10
  184. package/skills-templates/chore-planning/SKILL.md +47 -18
  185. package/skills-templates/design-system-selection/SKILL.md +273 -0
  186. package/skills-templates/epic-planning/SKILL.md +82 -48
  187. package/skills-templates/external-transition/SKILL.md +47 -47
  188. package/skills-templates/feature-planning/SKILL.md +173 -74
  189. package/skills-templates/production-mode/SKILL.md +69 -49
  190. package/skills-templates/request-routing/SKILL.md +4 -4
  191. package/skills-templates/simple-improvement/SKILL.md +74 -29
  192. package/skills-templates/speed-mode/SKILL.md +217 -141
  193. package/skills-templates/stable-mode/SKILL.md +148 -89
  194. package/apps/dashboard/README.md +0 -36
  195. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
  196. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  197. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
  198. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  199. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
  200. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  201. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  202. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  203. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  204. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  205. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  206. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  207. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  208. package/apps/dashboard/app/api/tests/route.ts +0 -9
  209. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  210. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  211. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  212. package/apps/dashboard/app/api/usage/route.ts +0 -17
  213. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  214. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  215. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  216. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
  217. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  218. package/apps/dashboard/app/layout.tsx +0 -43
  219. package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
  220. package/apps/dashboard/electron/ipc-handlers.js +0 -1028
  221. package/apps/dashboard/electron/main.js +0 -2124
  222. package/apps/dashboard/electron/preload.js +0 -123
  223. package/apps/dashboard/electron/session-manager.js +0 -141
  224. package/apps/dashboard/electron-builder.config.js +0 -357
  225. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  226. package/apps/dashboard/lib/claude-process-manager.ts +0 -492
  227. package/apps/dashboard/lib/db-bridge.ts +0 -282
  228. package/apps/dashboard/lib/prototypes.ts +0 -202
  229. package/apps/dashboard/lib/test-results-db.ts +0 -307
  230. package/apps/dashboard/lib/tests.ts +0 -282
  231. package/apps/dashboard/next.config.js +0 -50
  232. package/apps/dashboard/postcss.config.mjs +0 -7
  233. package/apps/dashboard/public/file.svg +0 -1
  234. package/apps/dashboard/public/globe.svg +0 -1
  235. package/apps/dashboard/public/next.svg +0 -1
  236. package/apps/dashboard/public/vercel.svg +0 -1
  237. package/apps/dashboard/public/window.svg +0 -1
  238. package/apps/dashboard/scripts/download-node.js +0 -104
  239. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
  240. package/docs/bdd-guidance.md +0 -390
@@ -1,7 +1,6 @@
1
- 'use client';
2
1
 
3
- import { useState, useRef, useCallback, KeyboardEvent, ChangeEvent } from 'react';
4
- import { motion, AnimatePresence } from 'framer-motion';
2
+ import { useState, useRef, useCallback, useEffect, KeyboardEvent, ChangeEvent } from 'react';
3
+ import { m, AnimatePresence } from 'framer-motion';
5
4
 
6
5
  export interface AttachedImage {
7
6
  id: string;
@@ -19,6 +18,7 @@ interface ClaudePanelInputProps {
19
18
  placeholder?: string;
20
19
  attachedImages?: AttachedImage[];
21
20
  onImagesChange?: (images: AttachedImage[]) => void;
21
+ activeSessionId?: string | null;
22
22
  }
23
23
 
24
24
  const DOUBLE_ESCAPE_THRESHOLD_MS = 500;
@@ -31,6 +31,7 @@ export function ClaudePanelInput({
31
31
  placeholder = 'Type a message...',
32
32
  attachedImages: externalImages,
33
33
  onImagesChange,
34
+ activeSessionId,
34
35
  }: ClaudePanelInputProps) {
35
36
  const [message, setMessage] = useState('');
36
37
  const [isFocused, setIsFocused] = useState(false);
@@ -38,10 +39,37 @@ export function ClaudePanelInput({
38
39
  const textareaRef = useRef<HTMLTextAreaElement>(null);
39
40
  const lastEscapeTimeRef = useRef<number>(0);
40
41
 
42
+ // Per-session draft map: save/restore draft text when switching tabs
43
+ const draftsRef = useRef(new Map<string, string>());
44
+ const prevSessionRef = useRef(activeSessionId);
45
+ const messageRef = useRef(message);
46
+ messageRef.current = message;
47
+
48
+ useEffect(() => {
49
+ const prevId = prevSessionRef.current;
50
+ if (prevId && prevId !== activeSessionId) {
51
+ draftsRef.current.set(prevId, messageRef.current);
52
+ }
53
+ const restored = activeSessionId ? draftsRef.current.get(activeSessionId) ?? '' : '';
54
+ setMessage(restored);
55
+ if (textareaRef.current) {
56
+ textareaRef.current.style.height = 'auto';
57
+ }
58
+ prevSessionRef.current = activeSessionId;
59
+ }, [activeSessionId]);
60
+
41
61
  // Use external image state if provided (panel-level drag-drop), otherwise internal
42
62
  const attachedImages = externalImages ?? internalImages;
43
63
  const setAttachedImages = onImagesChange ?? setInternalImages;
44
64
 
65
+ // Auto-focus textarea on mount and when active session changes.
66
+ // The mount case handles when ClaudePanelInput replaces ReviewFooter after rejection.
67
+ useEffect(() => {
68
+ if (textareaRef.current) {
69
+ textareaRef.current.focus();
70
+ }
71
+ }, [activeSessionId]);
72
+
45
73
  const handleSend = useCallback(() => {
46
74
  const trimmed = message.trim();
47
75
  const hasContent = trimmed || attachedImages.length > 0;
@@ -93,20 +121,20 @@ export function ClaudePanelInput({
93
121
 
94
122
  return (
95
123
  <div
96
- className="border-t border-zinc-200 bg-zinc-50 p-3"
124
+ className="border-t border-zinc-200 bg-zinc-50 p-4"
97
125
  data-testid="claude-panel-input"
98
126
  >
99
127
  <div
100
128
  className={`
101
- relative rounded-lg border transition-all duration-200
102
- ${isFocused ? 'border-blue-500 bg-white' : 'border-zinc-300 bg-white'}
129
+ relative rounded-lg border-2 transition-[border-color] duration-200 ease-out
130
+ ${isFocused ? 'border-[#819D9F] bg-white' : 'border-zinc-300 bg-white'}
103
131
  ${disabled ? 'opacity-50 cursor-not-allowed' : ''}
104
132
  `}
105
133
  >
106
134
 
107
135
  {/* Thumbnail preview section */}
108
136
  {attachedImages.length > 0 && (
109
- <div className="flex flex-wrap gap-2 px-3 pt-2" data-testid="image-preview-section">
137
+ <div className="flex flex-wrap gap-3 px-4 pt-3" data-testid="image-preview-section">
110
138
  {attachedImages.map(image => (
111
139
  <div
112
140
  key={image.id}
@@ -116,12 +144,12 @@ export function ClaudePanelInput({
116
144
  <img
117
145
  src={image.dataUrl}
118
146
  alt={image.name}
119
- className="w-16 h-16 object-cover rounded border border-zinc-200"
147
+ className="w-16 h-16 object-cover rounded"
120
148
  />
121
149
  <button
122
150
  type="button"
123
151
  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"
152
+ 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
153
  data-testid="remove-image-button"
126
154
  aria-label={`Remove ${image.name}`}
127
155
  >
@@ -143,10 +171,10 @@ export function ClaudePanelInput({
143
171
  disabled={disabled}
144
172
  rows={1}
145
173
  className={`
146
- w-full resize-none bg-transparent px-3 py-2.5 text-sm text-zinc-900
174
+ w-full resize-none bg-transparent px-4 py-3 text-base text-zinc-900
147
175
  placeholder:text-zinc-400 focus:outline-none
148
176
  ${isExpanded ? 'min-h-[80px]' : 'min-h-[40px]'}
149
- transition-all duration-200
177
+ transition-[min-height] duration-200 ease-out
150
178
  `}
151
179
  style={{ maxHeight: '200px' }}
152
180
  data-testid="claude-input-textarea"
@@ -155,12 +183,12 @@ export function ClaudePanelInput({
155
183
  {/* Footer with character count and hints - only show when expanded */}
156
184
  <AnimatePresence>
157
185
  {isExpanded && (
158
- <motion.div
186
+ <m.div
159
187
  initial={{ opacity: 0, height: 0 }}
160
188
  animate={{ opacity: 1, height: 'auto' }}
161
189
  exit={{ opacity: 0, height: 0 }}
162
190
  transition={{ duration: 0.15 }}
163
- className="flex items-center justify-between px-3 pb-2 text-xs text-zinc-500"
191
+ className="flex items-center justify-between px-4 pb-3 text-xs text-zinc-500"
164
192
  >
165
193
  <div className="flex items-center gap-3" data-testid="keyboard-hints">
166
194
  <span>
@@ -177,14 +205,14 @@ export function ClaudePanelInput({
177
205
  <span>
178
206
  <kbd className="px-1.5 py-0.5 rounded bg-zinc-200 text-zinc-600 font-mono text-[10px]">Esc</kbd>
179
207
  <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
208
+ {' '}to interrupt
181
209
  </span>
182
210
  )}
183
211
  </div>
184
212
  <span data-testid="character-count">
185
213
  {characterCount > 0 ? `${characterCount} chars` : ''}
186
214
  </span>
187
- </motion.div>
215
+ </m.div>
188
216
  )}
189
217
  </AnimatePresence>
190
218
  </div>
@@ -1,7 +1,5 @@
1
- 'use client';
2
-
3
1
  import { useState, useEffect, useRef } from 'react';
4
- import Image from 'next/image';
2
+ import { Button } from '@/components/ui/Button';
5
3
 
6
4
  type ConnectState = 'idle' | 'waiting' | 'success' | 'error';
7
5
 
@@ -89,15 +87,14 @@ export function ConnectClaudeScreen({ onConnect, onCheckAuth }: ConnectClaudeScr
89
87
 
90
88
  return (
91
89
  <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">
90
+ <div className="max-w-md w-full space-y-10">
93
91
  {/* Logo */}
94
- <div className="flex flex-col items-center space-y-4">
95
- <Image
92
+ <div className="flex flex-col items-center space-y-6">
93
+ <img
96
94
  src="/jettypod_wordmark.png"
97
95
  alt="JettyPod"
98
96
  width={160}
99
97
  height={40}
100
- priority
101
98
  />
102
99
  <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
103
100
  Connect Claude Code
@@ -111,7 +108,7 @@ export function ConnectClaudeScreen({ onConnect, onCheckAuth }: ConnectClaudeScr
111
108
  {/* Error Banner */}
112
109
  {errorMessage && (
113
110
  <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"
111
+ 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"
115
112
  data-testid="connect-error"
116
113
  >
117
114
  {errorMessage}
@@ -119,7 +116,7 @@ export function ConnectClaudeScreen({ onConnect, onCheckAuth }: ConnectClaudeScr
119
116
  )}
120
117
 
121
118
  {/* Progress Stepper */}
122
- <div className="flex flex-col gap-4">
119
+ <div className="flex flex-col gap-5">
123
120
  {steps.map((label, i) => {
124
121
  const stepNum = i + 1;
125
122
  const isDone = completedSteps.includes(stepNum);
@@ -131,14 +128,14 @@ export function ConnectClaudeScreen({ onConnect, onCheckAuth }: ConnectClaudeScr
131
128
  return (
132
129
  <div
133
130
  key={stepNum}
134
- className={`flex items-center gap-3 text-sm ${
131
+ className={`flex items-center gap-4 text-base ${
135
132
  isDone ? 'text-zinc-900 dark:text-zinc-100' :
136
133
  isActive ? 'text-zinc-900 dark:text-zinc-100 font-medium' :
137
134
  'text-zinc-400 dark:text-zinc-500'
138
135
  }`}
139
136
  >
140
137
  <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 ${
138
+ 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 ${
142
139
  isDone ? 'bg-green-400 text-white' :
143
140
  isActive ? 'bg-[#c8d9da] text-[#3d4d4e]' :
144
141
  'bg-zinc-200 dark:bg-zinc-700 text-zinc-400 dark:text-zinc-500'
@@ -153,7 +150,7 @@ export function ConnectClaudeScreen({ onConnect, onCheckAuth }: ConnectClaudeScr
153
150
  </div>
154
151
 
155
152
  {/* Connect Button */}
156
- <div className="pt-4">
153
+ <div className="pt-6">
157
154
  {state === 'success' ? (
158
155
  <div
159
156
  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"
@@ -162,29 +159,15 @@ export function ConnectClaudeScreen({ onConnect, onCheckAuth }: ConnectClaudeScr
162
159
  ✓ Connected!
163
160
  </div>
164
161
  ) : (
165
- <button
162
+ <Button
166
163
  onClick={handleConnect}
167
164
  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
- }}
165
+ size="lg"
166
+ fullWidth
184
167
  data-testid="connect-claude-button"
185
168
  >
186
169
  {state === 'waiting' ? (
187
- <span className="flex items-center justify-center gap-2">
170
+ <span className="flex items-center justify-center gap-3">
188
171
  <span className="inline-block w-4 h-4 border-2 border-[#c8d9da] border-t-[#3d4d4e] rounded-full animate-spin" />
189
172
  Waiting for login...
190
173
  </span>
@@ -193,20 +176,20 @@ export function ConnectClaudeScreen({ onConnect, onCheckAuth }: ConnectClaudeScr
193
176
  ) : (
194
177
  'Connect Claude Code'
195
178
  )}
196
- </button>
179
+ </Button>
197
180
  )}
198
181
  </div>
199
182
 
200
183
  {/* Status text */}
201
184
  {state === 'waiting' && (
202
- <p className="text-sm text-zinc-400 dark:text-zinc-500 text-center">
185
+ <p className="text-base text-zinc-400 dark:text-zinc-500 text-center">
203
186
  A browser window should have opened. Complete the sign-in there.
204
187
  </p>
205
188
  )}
206
189
 
207
190
  {/* 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">
191
+ <div className="pt-10 space-y-4">
192
+ <div className="border-2 border-zinc-200 dark:border-zinc-700 rounded-xl p-6 text-zinc-500 dark:text-zinc-400 text-base">
210
193
  <p>
211
194
  <strong className="text-zinc-700 dark:text-zinc-300">Why do I need this?</strong>
212
195
  </p>
@@ -1,4 +1,3 @@
1
- 'use client';
2
1
 
3
2
  import { useState } from 'react';
4
3
 
@@ -51,15 +50,15 @@ export function CopyableId({ id, title, type, size = 'sm' }: CopyableIdProps) {
51
50
  };
52
51
 
53
52
  const sizeClasses = size === 'md'
54
- ? 'text-sm px-1.5 py-1'
55
- : 'text-xs px-1 py-0.5';
53
+ ? 'text-base px-1.5 py-1'
54
+ : 'text-sm px-1 py-0.5';
56
55
 
57
56
  const iconClasses = size === 'md' ? 'w-4 h-4' : 'w-3 h-3';
58
57
 
59
58
  return (
60
59
  <button
61
60
  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}`}
61
+ 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] duration-200 ease-out ${sizeClasses}`}
63
62
  title={`Copy: #${id} ${title} (${type})`}
64
63
  >
65
64
  <span>#{id}</span>
@@ -0,0 +1,100 @@
1
+ import { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { Button } from '@/components/ui/Button';
4
+ import { Input } from '@/components/ui/Input';
5
+ import { dataBridge } from '@/lib/data-bridge';
6
+
7
+ interface DetailReviewActionsProps {
8
+ workItemId: number;
9
+ }
10
+
11
+ export function DetailReviewActions({ workItemId }: DetailReviewActionsProps) {
12
+ const navigate = useNavigate();
13
+ const [showRejectInput, setShowRejectInput] = useState(false);
14
+ const [rejectReason, setRejectReason] = useState('');
15
+ const [isSubmitting, setIsSubmitting] = useState(false);
16
+
17
+ const handleAccept = async () => {
18
+ setIsSubmitting(true);
19
+ try {
20
+ await dataBridge.updateStatus(workItemId, 'done');
21
+ navigate('/');
22
+ } catch {
23
+ setIsSubmitting(false);
24
+ }
25
+ };
26
+
27
+ const handleRejectConfirm = async () => {
28
+ if (!rejectReason.trim()) return;
29
+ setIsSubmitting(true);
30
+ try {
31
+ await dataBridge.updateStatus(workItemId, 'in_progress', rejectReason.trim());
32
+ navigate(`/?rejected=${workItemId}&reason=${encodeURIComponent(rejectReason.trim())}`);
33
+ } catch {
34
+ setIsSubmitting(false);
35
+ }
36
+ };
37
+
38
+ if (showRejectInput) {
39
+ return (
40
+ <div className="flex items-center gap-2" data-testid="detail-review-reject-area">
41
+ <Input
42
+ type="text"
43
+ value={rejectReason}
44
+ onChange={(e) => setRejectReason(e.target.value)}
45
+ onKeyDown={(e) => {
46
+ if (e.key === 'Enter' && rejectReason.trim()) handleRejectConfirm();
47
+ if (e.key === 'Escape') {
48
+ setShowRejectInput(false);
49
+ setRejectReason('');
50
+ }
51
+ }}
52
+ placeholder="Rejection reason..."
53
+ size="sm"
54
+ error
55
+ autoFocus
56
+ data-testid="detail-review-reject-input"
57
+ />
58
+ <Button
59
+ onClick={handleRejectConfirm}
60
+ disabled={!rejectReason.trim()}
61
+ loading={isSubmitting}
62
+ variant="destructive"
63
+ size="sm"
64
+ data-testid="detail-review-reject-confirm"
65
+ >
66
+ Reject
67
+ </Button>
68
+ <Button
69
+ onClick={() => { setShowRejectInput(false); setRejectReason(''); }}
70
+ variant="ghost"
71
+ size="sm"
72
+ data-testid="detail-review-reject-cancel"
73
+ >
74
+ Cancel
75
+ </Button>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ return (
81
+ <div className="flex items-center gap-2" data-testid="detail-review-actions">
82
+ <Button
83
+ onClick={handleAccept}
84
+ loading={isSubmitting}
85
+ size="sm"
86
+ data-testid="detail-review-accept"
87
+ >
88
+ Accept
89
+ </Button>
90
+ <Button
91
+ onClick={() => setShowRejectInput(true)}
92
+ variant="secondary"
93
+ size="sm"
94
+ data-testid="detail-review-reject"
95
+ >
96
+ Reject
97
+ </Button>
98
+ </div>
99
+ );
100
+ }