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
@@ -0,0 +1,139 @@
1
+
2
+ import { useState } from 'react';
3
+ import { Button } from '@/components/ui/Button';
4
+ import { Input } from '@/components/ui/Input';
5
+ import { dataBridge } from '@/lib/data-bridge';
6
+
7
+ interface ReviewFooterProps {
8
+ workItemId: string;
9
+ onAccepted: () => void;
10
+ onRejected: (reason: string) => void;
11
+ onAskQuestion: () => void;
12
+ }
13
+
14
+ export function ReviewFooter({ workItemId, onAccepted, onRejected, onAskQuestion }: ReviewFooterProps) {
15
+ const [showRejectInput, setShowRejectInput] = useState(false);
16
+ const [rejectReason, setRejectReason] = useState('');
17
+ const [isSubmitting, setIsSubmitting] = useState(false);
18
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
19
+
20
+ const handleAccept = async () => {
21
+ setErrorMessage(null);
22
+ setIsSubmitting(true);
23
+ try {
24
+ await dataBridge.updateStatus(parseInt(workItemId, 10), 'done');
25
+ onAccepted();
26
+ } catch {
27
+ setErrorMessage('Failed to accept. Please try again.');
28
+ setIsSubmitting(false);
29
+ }
30
+ };
31
+
32
+ const handleRejectClick = () => {
33
+ setShowRejectInput(true);
34
+ };
35
+
36
+ const handleRejectConfirm = async () => {
37
+ if (!rejectReason.trim()) return;
38
+ setErrorMessage(null);
39
+ setIsSubmitting(true);
40
+ try {
41
+ await dataBridge.updateStatus(parseInt(workItemId, 10), 'in_progress', rejectReason.trim());
42
+ const reason = rejectReason.trim();
43
+ setIsSubmitting(false);
44
+ setShowRejectInput(false);
45
+ setRejectReason('');
46
+ onRejected(reason);
47
+ } catch {
48
+ setErrorMessage('Failed to reject. Please try again.');
49
+ setIsSubmitting(false);
50
+ }
51
+ };
52
+
53
+ const handleRejectCancel = () => {
54
+ setShowRejectInput(false);
55
+ setRejectReason('');
56
+ };
57
+
58
+ return (
59
+ <div className="border-t border-zinc-200 px-5 py-5 flex-shrink-0" data-testid="review-footer">
60
+ <p className="text-base font-semibold text-zinc-900 mb-1.5">
61
+ This work is done. How does it look?
62
+ </p>
63
+ <p className="text-base text-zinc-500 mb-4">
64
+ Just a heads up. This chat will close after acceptance.
65
+ </p>
66
+
67
+ {errorMessage && (
68
+ <p className="text-sm text-red-600 mb-3" data-testid="review-error-message">{errorMessage}</p>
69
+ )}
70
+
71
+ {!showRejectInput ? (
72
+ <div className="flex items-center gap-2" data-testid="review-actions">
73
+ <Button
74
+ onClick={handleAccept}
75
+ loading={isSubmitting}
76
+ data-testid="review-accept-button"
77
+ >
78
+ Accept
79
+ </Button>
80
+ <Button
81
+ onClick={handleRejectClick}
82
+ variant="secondary"
83
+ data-testid="review-reject-button"
84
+ >
85
+ Reject
86
+ </Button>
87
+ <Button
88
+ onClick={onAskQuestion}
89
+ variant="ghost"
90
+ data-testid="review-ask-question-button"
91
+ >
92
+ Ask a question
93
+ </Button>
94
+ </div>
95
+ ) : (
96
+ <div data-testid="review-reject-input-area">
97
+ <Input
98
+ type="text"
99
+ value={rejectReason}
100
+ onChange={(e) => setRejectReason(e.target.value)}
101
+ onKeyDown={(e) => {
102
+ if (e.key === 'Enter' && rejectReason.trim()) {
103
+ handleRejectConfirm();
104
+ }
105
+ if (e.key === 'Escape') {
106
+ handleRejectCancel();
107
+ }
108
+ }}
109
+ placeholder="Rejection reason..."
110
+ size="sm"
111
+ error
112
+ autoFocus
113
+ data-testid="review-reject-reason-input"
114
+ />
115
+ <div className="flex items-center gap-1.5 mt-2">
116
+ <Button
117
+ onClick={handleRejectConfirm}
118
+ disabled={!rejectReason.trim()}
119
+ loading={isSubmitting}
120
+ variant="destructive"
121
+ size="sm"
122
+ data-testid="review-reject-confirm"
123
+ >
124
+ Reject
125
+ </Button>
126
+ <Button
127
+ onClick={handleRejectCancel}
128
+ variant="ghost"
129
+ size="sm"
130
+ data-testid="review-reject-cancel"
131
+ >
132
+ Cancel
133
+ </Button>
134
+ </div>
135
+ </div>
136
+ )}
137
+ </div>
138
+ );
139
+ }
@@ -1,6 +1,6 @@
1
- 'use client';
2
1
 
3
- import { motion } from 'framer-motion';
2
+ import { m } from 'framer-motion';
3
+ import { Button } from '@/components/ui/Button';
4
4
 
5
5
  export interface SessionItem {
6
6
  id: string;
@@ -26,23 +26,23 @@ export function SessionList({
26
26
  return (
27
27
  <div className="flex-1 flex flex-col" data-testid="session-list">
28
28
  {/* Header */}
29
- <div className="px-4 py-3 border-b border-zinc-800">
29
+ <div className="px-5 py-4 border-b border-zinc-800">
30
30
  <div className="flex items-center justify-between">
31
- <h2 className="text-sm font-semibold text-white">Sessions</h2>
32
- <button
31
+ <h2 className="text-base font-semibold text-white">Sessions</h2>
32
+ <Button
33
33
  onClick={onNewSession}
34
- className="px-3 py-1.5 text-xs font-medium bg-blue-600 hover:bg-blue-500 text-white rounded transition-colors"
34
+ size="sm"
35
35
  data-testid="new-session-button"
36
36
  >
37
37
  New Session
38
- </button>
38
+ </Button>
39
39
  </div>
40
40
  </div>
41
41
 
42
42
  {/* Session List */}
43
43
  <div className="flex-1 overflow-y-auto">
44
44
  {sessions.length === 0 ? (
45
- <div className="p-4 text-center text-zinc-500 text-sm" data-testid="empty-session-state">
45
+ <div className="p-6 text-center text-zinc-500 text-base" data-testid="empty-session-state">
46
46
  No sessions yet. Click New Session to start.
47
47
  </div>
48
48
  ) : (
@@ -50,39 +50,39 @@ export function SessionList({
50
50
  {sessions.map((session) => (
51
51
  <div
52
52
  key={session.id}
53
- className="flex items-center hover:bg-zinc-800/50 transition-colors"
53
+ className="flex items-center hover:bg-zinc-800/50 transition-colors duration-200 ease-out"
54
54
  data-testid={`session-item-${session.id}`}
55
55
  >
56
- <motion.button
56
+ <m.button
57
57
  onClick={() => onSelectSession(session.id)}
58
- className="flex-1 px-4 py-3 text-left"
58
+ className="flex-1 px-5 py-4 text-left"
59
59
  whileHover={{ x: 4 }}
60
60
  >
61
- <div className="flex items-center gap-2">
61
+ <div className="flex items-center gap-3">
62
62
  <SessionIcon hasFeature={!!session.featureId} />
63
63
  <div className="flex-1 min-w-0">
64
- <p className="text-sm font-medium text-white truncate">
64
+ <p className="text-base font-medium text-white truncate">
65
65
  {session.featureId ? session.featureTitle : session.title}
66
66
  </p>
67
67
  {session.featureId && (
68
- <span className="inline-flex items-center px-1.5 py-0.5 mt-1 text-xs font-medium bg-blue-900/50 text-blue-300 rounded">
68
+ <span className="inline-flex items-center px-2 py-1 mt-1.5 text-xs font-medium bg-[#819D9F]/20 text-[#a3bfc0] rounded">
69
69
  #{session.featureId}
70
70
  </span>
71
71
  )}
72
72
  {!session.featureId && (
73
- <p className="text-xs text-zinc-500 mt-0.5">Unlinked session</p>
73
+ <p className="text-base text-zinc-500 mt-1">Unlinked session</p>
74
74
  )}
75
75
  </div>
76
76
  <ChevronIcon />
77
77
  </div>
78
- </motion.button>
78
+ </m.button>
79
79
  {onCloseSession && (
80
80
  <button
81
81
  onClick={(e) => {
82
82
  e.stopPropagation();
83
83
  onCloseSession(session.id);
84
84
  }}
85
- className="p-2 mr-2 rounded hover:bg-zinc-700 text-zinc-500 hover:text-zinc-300 transition-colors"
85
+ className="p-2.5 mr-3 rounded hover:bg-zinc-700 text-zinc-500 hover:text-zinc-300 transition-colors duration-200 ease-out"
86
86
  aria-label="Close session"
87
87
  data-testid={`close-session-${session.id}`}
88
88
  >
@@ -101,10 +101,10 @@ export function SessionList({
101
101
  function SessionIcon({ hasFeature }: { hasFeature: boolean }) {
102
102
  return (
103
103
  <div className={`w-8 h-8 rounded-full flex items-center justify-center ${
104
- hasFeature ? 'bg-blue-900/50' : 'bg-zinc-800'
104
+ hasFeature ? 'bg-[#819D9F]/20' : 'bg-zinc-800'
105
105
  }`}>
106
106
  <svg
107
- className={`w-4 h-4 ${hasFeature ? 'text-blue-400' : 'text-zinc-400'}`}
107
+ className={`w-4 h-4 ${hasFeature ? 'text-[#819D9F]' : 'text-zinc-400'}`}
108
108
  fill="none"
109
109
  stroke="currentColor"
110
110
  viewBox="0 0 24 24"
@@ -1,25 +1,10 @@
1
- 'use client';
2
1
 
3
2
  import { useState, useEffect, useRef } from 'react';
4
-
3
+ import { Button } from '@/components/ui/Button';
4
+ import { isTauri, auth, shell } from '@/lib/tauri-bridge';
5
5
 
6
6
  const API_BASE = 'https://jettypod-update-server.spangbaryn2.workers.dev';
7
7
 
8
- const buttonGradientStyle = {
9
- background: 'linear-gradient(145deg, #ffffff 0%, #faf9f7 10%, #f0f4f4 35%, #c8d9da 55%, #819D9F 90%)',
10
- color: '#3d4d4e',
11
- boxShadow: `
12
- 0 1px 1px rgba(0, 0, 0, 0.02),
13
- 0 2px 4px rgba(0, 0, 0, 0.03),
14
- 0 6px 12px rgba(0, 0, 0, 0.05),
15
- 0 12px 24px rgba(0, 0, 0, 0.06),
16
- 0 20px 40px rgba(129, 157, 159, 0.2),
17
- 0 32px 64px rgba(129, 157, 159, 0.18),
18
- inset 0 2px 4px rgba(255, 255, 255, 1),
19
- inset 0 -2px 4px rgba(129, 157, 159, 0.05)
20
- `,
21
- };
22
-
23
8
  type PageState = 'idle' | 'checkout-opened' | 'polling' | 'upgraded';
24
9
  type PlanType = 'monthly' | 'lifetime';
25
10
 
@@ -35,14 +20,38 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
35
20
  const [userPlan, setUserPlan] = useState<string | null>(null);
36
21
  const [pollingPlan, setPollingPlan] = useState<PlanType | null>(null);
37
22
  const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
23
+ const abortRef = useRef<AbortController | null>(null);
38
24
 
39
25
  useEffect(() => {
40
26
  async function loadAuth() {
41
- if (!window.electronAPI?.isElectron) return;
42
- const status = await window.electronAPI.auth.getStatus();
27
+ if (!isTauri()) return;
28
+ const status = await auth.getStatus();
43
29
  if (status.authenticated && status.user) {
44
30
  setUserEmail(status.user.email);
45
31
  setUserPlan(status.user.plan || 'free');
32
+
33
+ // Check server-side plan in case webhook updated it after local JWT was issued
34
+ const token = await auth.getToken();
35
+ if (token) {
36
+ try {
37
+ const res = await fetch(`${API_BASE}/auth/me`, {
38
+ headers: { 'Authorization': `Bearer ${token}` },
39
+ });
40
+ if (res.ok) {
41
+ const data = await res.json() as { user: { plan: string; email: string }; token?: string };
42
+ if (data.user.plan !== 'free') {
43
+ if (data.token) {
44
+ await auth.saveToken(data.token, data.user);
45
+ }
46
+ setUserPlan(data.user.plan);
47
+ setPageState('upgraded');
48
+ return;
49
+ }
50
+ }
51
+ } catch {
52
+ // Fall through to show upgrade UI on network error
53
+ }
54
+ }
46
55
  }
47
56
  }
48
57
  loadAuth();
@@ -51,19 +60,22 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
51
60
  useEffect(() => {
52
61
  return () => {
53
62
  if (pollingRef.current) clearInterval(pollingRef.current);
63
+ abortRef.current?.abort();
54
64
  };
55
65
  }, []);
56
66
 
57
67
  const startPolling = () => {
58
68
  setPageState('polling');
69
+ abortRef.current = new AbortController();
59
70
 
60
71
  pollingRef.current = setInterval(async () => {
61
72
  try {
62
- const token = await window.electronAPI!.auth.getToken();
73
+ const token = await auth.getToken();
63
74
  if (!token) return;
64
75
 
65
76
  const res = await fetch(`${API_BASE}/auth/me`, {
66
77
  headers: { 'Authorization': `Bearer ${token}` },
78
+ signal: abortRef.current?.signal,
67
79
  });
68
80
 
69
81
  if (!res.ok) return;
@@ -72,7 +84,7 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
72
84
 
73
85
  if (data.user.plan !== 'free') {
74
86
  if (data.token) {
75
- await window.electronAPI!.auth.saveToken(data.token, data.user);
87
+ await auth.saveToken(data.token, data.user);
76
88
  }
77
89
  setUserPlan(data.user.plan);
78
90
  setPageState('upgraded');
@@ -85,13 +97,38 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
85
97
  };
86
98
 
87
99
  const handleCheckout = async (plan: 'monthly' | 'lifetime') => {
88
- if (!window.electronAPI?.isElectron) return;
100
+ if (!isTauri()) return;
89
101
  setCheckoutPlan(plan);
90
102
  setError(null);
91
103
 
92
- const result = await window.electronAPI.subscription.createCheckout(plan);
93
- if (!result.success) {
94
- setError(result.error || 'Failed to start checkout.');
104
+ try {
105
+ const token = await auth.getToken();
106
+ if (!token) {
107
+ setError('Not authenticated');
108
+ setCheckoutPlan(null);
109
+ return;
110
+ }
111
+ const res = await fetch(`${API_BASE}/billing/create-checkout`, {
112
+ method: 'POST',
113
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
114
+ body: JSON.stringify({ plan }),
115
+ });
116
+ if (res.ok) {
117
+ const data = await res.json() as { url?: string };
118
+ if (data.url) {
119
+ await shell.openUrl(data.url);
120
+ } else {
121
+ setError('Failed to start checkout.');
122
+ setCheckoutPlan(null);
123
+ return;
124
+ }
125
+ } else {
126
+ setError('Failed to start checkout.');
127
+ setCheckoutPlan(null);
128
+ return;
129
+ }
130
+ } catch {
131
+ setError('Failed to start checkout.');
95
132
  setCheckoutPlan(null);
96
133
  return;
97
134
  }
@@ -111,20 +148,16 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
111
148
 
112
149
  if (pageState === 'upgraded') {
113
150
  return (
114
- <div className="max-w-md w-full space-y-8 text-center">
151
+ <div className="max-w-md w-full space-y-10 text-center">
115
152
  <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100">
116
153
  You&apos;re all set!
117
154
  </h1>
118
155
  <p className="text-zinc-500 dark:text-zinc-400">
119
156
  Your plan has been upgraded to <span className="font-medium text-zinc-900 dark:text-zinc-100">{userPlan}</span>.
120
157
  </p>
121
- <button
122
- onClick={handleDone}
123
- 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"
124
- style={buttonGradientStyle}
125
- >
158
+ <Button onClick={handleDone} size="lg" fullWidth>
126
159
  {onClose ? 'Done' : 'Go to Dashboard'}
127
- </button>
160
+ </Button>
128
161
  </div>
129
162
  );
130
163
  }
@@ -132,8 +165,8 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
132
165
  const isPolling = pageState === 'polling';
133
166
 
134
167
  return (
135
- <div className="max-w-md w-full space-y-8">
136
- <div className="flex flex-col items-center space-y-4">
168
+ <div className="max-w-md w-full space-y-10">
169
+ <div className="flex flex-col items-center space-y-6">
137
170
  <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
138
171
  Unlock Unlimited Use
139
172
  </h1>
@@ -143,7 +176,7 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
143
176
  </div>
144
177
 
145
178
  {userEmail && (
146
- <div className="bg-zinc-50 dark:bg-zinc-800 rounded-xl px-4 py-3 text-sm">
179
+ <div className="bg-zinc-50 dark:bg-zinc-800 rounded-xl px-5 py-4 text-base">
147
180
  <div className="flex justify-between items-center">
148
181
  <span className="text-zinc-500 dark:text-zinc-400">{userEmail}</span>
149
182
  <span className="text-zinc-400 dark:text-zinc-500 capitalize">{userPlan}</span>
@@ -152,39 +185,50 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
152
185
  )}
153
186
 
154
187
  {error && (
155
- <div 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">
188
+ <div 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">
156
189
  {error}
157
190
  </div>
158
191
  )}
159
192
 
160
- <div className="pt-4 space-y-3">
161
- <button
193
+ <div className="pt-6 space-y-4">
194
+ {!onClose && (
195
+ <Button
196
+ variant="ghost"
197
+ size="sm"
198
+ fullWidth
199
+ className="mb-2"
200
+ onClick={() => window.history.back()}
201
+ >
202
+ &larr; Go back
203
+ </Button>
204
+ )}
205
+ <Button
162
206
  onClick={() => handleCheckout('monthly')}
163
207
  disabled={checkoutPlan !== null || isPolling}
164
- 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-50 disabled:pointer-events-none"
165
- style={{ cursor: (checkoutPlan || isPolling) ? 'default' : 'pointer', ...buttonGradientStyle }}
208
+ size="lg"
209
+ fullWidth
166
210
  >
167
211
  {pollingPlan === 'monthly' ? (
168
- <span className="inline-flex items-center gap-2">
212
+ <span className="inline-flex items-center gap-3">
169
213
  <span className="animate-spin inline-block h-4 w-4 border-2 border-current border-t-transparent rounded-full" />
170
214
  Upgrading...
171
215
  </span>
172
216
  ) : checkoutPlan === 'monthly' ? 'Opening checkout...' : 'Monthly'}
173
- </button>
217
+ </Button>
174
218
 
175
- <button
219
+ <Button
176
220
  onClick={() => handleCheckout('lifetime')}
177
221
  disabled={checkoutPlan !== null || isPolling}
178
- 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-50 disabled:pointer-events-none"
179
- style={{ cursor: (checkoutPlan || isPolling) ? 'default' : 'pointer', ...buttonGradientStyle }}
222
+ size="lg"
223
+ fullWidth
180
224
  >
181
225
  {pollingPlan === 'lifetime' ? (
182
- <span className="inline-flex items-center gap-2">
226
+ <span className="inline-flex items-center gap-3">
183
227
  <span className="animate-spin inline-block h-4 w-4 border-2 border-current border-t-transparent rounded-full" />
184
228
  Upgrading...
185
229
  </span>
186
230
  ) : checkoutPlan === 'lifetime' ? 'Opening checkout...' : 'Lifetime Access'}
187
- </button>
231
+ </Button>
188
232
  </div>
189
233
  </div>
190
234
  );
@@ -1,7 +1,7 @@
1
- 'use client';
2
1
 
3
2
  import { useState } from 'react';
4
- import type { TestDashboardData, TestEpic, TestFeature, TestScenario } from '@/lib/tests';
3
+ import type { TestDashboardData, TestEpic, TestFeature, TestScenario } from '@/lib/db';
4
+ import { TypeIcon } from './TypeIcon';
5
5
 
6
6
  const statusIcons: Record<string, string> = {
7
7
  pass: '✅',
@@ -27,7 +27,7 @@ function ScenarioNode({ scenario, depth }: ScenarioNodeProps) {
27
27
  return (
28
28
  <div className="select-none">
29
29
  <div
30
- className={`flex items-center gap-2 py-1.5 px-2 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors ${hasError ? 'cursor-pointer' : ''}`}
30
+ className={`flex items-center gap-2 py-2 px-3 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors duration-200 ease-out ${hasError ? 'cursor-pointer' : ''}`}
31
31
  style={{ paddingLeft: `${depth * 20 + 8}px` }}
32
32
  onClick={() => hasError && setExpanded(!expanded)}
33
33
  >
@@ -38,8 +38,8 @@ function ScenarioNode({ scenario, depth }: ScenarioNodeProps) {
38
38
  ) : (
39
39
  <span className="w-4" />
40
40
  )}
41
- <span className="text-sm">{statusIcons[scenario.status]}</span>
42
- <span className={`flex-1 text-sm ${statusColors[scenario.status]}`}>
41
+ <span className="text-base">{statusIcons[scenario.status]}</span>
42
+ <span className={`flex-1 text-base ${statusColors[scenario.status]}`}>
43
43
  {scenario.title}
44
44
  </span>
45
45
  <span className="text-xs text-zinc-400 font-mono">{scenario.duration}</span>
@@ -48,11 +48,11 @@ function ScenarioNode({ scenario, depth }: ScenarioNodeProps) {
48
48
  {/* Error details panel */}
49
49
  {expanded && hasError && (
50
50
  <div
51
- className="mt-1 mb-2 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded text-sm"
51
+ className="mt-1.5 mb-3 p-4 bg-red-50 dark:bg-red-900/20 border-2 border-red-200 dark:border-red-800 rounded text-base"
52
52
  style={{ marginLeft: `${depth * 20 + 28}px` }}
53
53
  >
54
54
  {scenario.failedStep && (
55
- <div className="mb-2">
55
+ <div className="mb-3">
56
56
  <span className="font-semibold text-red-700 dark:text-red-300">Failed step: </span>
57
57
  <span className="text-red-600 dark:text-red-400">{scenario.failedStep}</span>
58
58
  </div>
@@ -60,7 +60,7 @@ function ScenarioNode({ scenario, depth }: ScenarioNodeProps) {
60
60
  {scenario.error && (
61
61
  <div>
62
62
  <span className="font-semibold text-red-700 dark:text-red-300">Error: </span>
63
- <pre className="mt-1 p-2 bg-red-100 dark:bg-red-900/40 rounded text-xs text-red-800 dark:text-red-200 overflow-x-auto whitespace-pre-wrap">
63
+ <pre className="mt-1.5 p-3 bg-red-100 dark:bg-red-900/40 rounded text-xs text-red-800 dark:text-red-200 overflow-x-auto whitespace-pre-wrap">
64
64
  {scenario.error}
65
65
  </pre>
66
66
  </div>
@@ -87,7 +87,7 @@ function FeatureNode({ feature, depth }: FeatureNodeProps) {
87
87
  return (
88
88
  <div className="select-none">
89
89
  <div
90
- className="flex items-center gap-2 py-1.5 px-2 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors cursor-pointer"
90
+ className="flex items-center gap-2 py-2 px-3 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors duration-200 ease-out cursor-pointer"
91
91
  style={{ paddingLeft: `${depth * 20 + 8}px` }}
92
92
  onClick={() => setExpanded(!expanded)}
93
93
  >
@@ -98,12 +98,12 @@ function FeatureNode({ feature, depth }: FeatureNodeProps) {
98
98
  ) : (
99
99
  <span className="w-4" />
100
100
  )}
101
- <span className="text-sm">✨</span>
101
+ <span className="text-base"><TypeIcon type="feature" /></span>
102
102
  <span className={`flex-1 font-medium ${allPassing ? 'text-zinc-900 dark:text-zinc-100' : 'text-red-600 dark:text-red-400'}`}>
103
103
  {feature.title}
104
104
  </span>
105
105
  {/* Health badge */}
106
- <span className="text-xs px-1.5 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800">
106
+ <span className="text-xs px-2 py-1 rounded bg-zinc-100 dark:bg-zinc-800">
107
107
  <span className="text-green-600 dark:text-green-400">{passingCount}</span>
108
108
  <span className="text-zinc-400 mx-1">/</span>
109
109
  <span className={failingCount > 0 ? 'text-red-600 dark:text-red-400' : 'text-zinc-400'}>
@@ -135,7 +135,7 @@ function EpicNode({ epic }: EpicNodeProps) {
135
135
  return (
136
136
  <div className="select-none">
137
137
  <div
138
- className="flex items-center gap-2 py-2 px-2 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors cursor-pointer"
138
+ className="flex items-center gap-2 py-3 px-3 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors duration-200 ease-out cursor-pointer"
139
139
  onClick={() => setExpanded(!expanded)}
140
140
  >
141
141
  {hasFeatures ? (
@@ -145,12 +145,12 @@ function EpicNode({ epic }: EpicNodeProps) {
145
145
  ) : (
146
146
  <span className="w-4" />
147
147
  )}
148
- <span className="text-base">🎯</span>
148
+ <span className="text-base"><TypeIcon type="epic" /></span>
149
149
  <span className={`flex-1 font-semibold ${allPassing ? 'text-zinc-900 dark:text-zinc-100' : 'text-red-600 dark:text-red-400'}`}>
150
150
  {epic.title}
151
151
  </span>
152
152
  {/* Health badge */}
153
- <span className="text-xs px-2 py-1 rounded bg-zinc-100 dark:bg-zinc-800 font-mono">
153
+ <span className="text-xs px-3 py-1.5 rounded bg-zinc-100 dark:bg-zinc-800 font-mono">
154
154
  <span className="text-green-600 dark:text-green-400">{epic.healthBadge.passing}</span>
155
155
  <span className="text-zinc-400 mx-1">/</span>
156
156
  <span className={epic.healthBadge.failing > 0 ? 'text-red-600 dark:text-red-400' : 'text-zinc-400'}>
@@ -184,7 +184,7 @@ export function TestTree({ data }: TestTreeProps) {
184
184
  }
185
185
 
186
186
  return (
187
- <div className="font-mono text-sm space-y-2">
187
+ <div className="font-mono text-base space-y-2">
188
188
  {/* Epics */}
189
189
  {data.epics.map(epic => (
190
190
  <EpicNode key={epic.id} epic={epic} />
@@ -194,7 +194,7 @@ export function TestTree({ data }: TestTreeProps) {
194
194
  {data.standaloneFeatures.length > 0 && (
195
195
  <div className="mt-4">
196
196
  {data.epics.length > 0 && (
197
- <div className="text-xs text-zinc-400 uppercase tracking-wider mb-2 px-2">
197
+ <div className="text-base text-zinc-400 uppercase tracking-wider mb-2 px-2">
198
198
  Standalone Features
199
199
  </div>
200
200
  )}