jettypod 4.4.120 → 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 (208) hide show
  1. package/.env +2 -1
  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 +54 -49
  8. package/apps/dashboard/app/demo/gates/page.tsx +3 -5
  9. package/apps/dashboard/app/design-system/page.tsx +1 -1
  10. package/apps/dashboard/app/globals.css +74 -2
  11. package/apps/dashboard/app/install-claude/page.tsx +3 -5
  12. package/apps/dashboard/app/login/page.tsx +17 -20
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +60 -12
  15. package/apps/dashboard/app/signup/page.tsx +14 -17
  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 +12 -15
  19. package/apps/dashboard/app/work/[id]/page.tsx +90 -75
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +70 -61
  22. package/apps/dashboard/components/CardMenu.tsx +0 -1
  23. package/apps/dashboard/components/ClaudePanel.tsx +541 -283
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
  26. package/apps/dashboard/components/CopyableId.tsx +1 -2
  27. package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
  28. package/apps/dashboard/components/DragContext.tsx +132 -62
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +5 -6
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +0 -1
  34. package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
  35. package/apps/dashboard/components/EpicGroup.tsx +100 -70
  36. package/apps/dashboard/components/GateCard.tsx +0 -1
  37. package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
  39. package/apps/dashboard/components/JettyLoader.tsx +0 -1
  40. package/apps/dashboard/components/KanbanBoard.tsx +319 -173
  41. package/apps/dashboard/components/KanbanCard.tsx +341 -107
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
  44. package/apps/dashboard/components/MainNav.tsx +24 -25
  45. package/apps/dashboard/components/MessageBlock.tsx +93 -16
  46. package/apps/dashboard/components/ModeStartCard.tsx +0 -1
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
  48. package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
  53. package/apps/dashboard/components/ReviewFooter.tsx +12 -14
  54. package/apps/dashboard/components/SessionList.tsx +0 -1
  55. package/apps/dashboard/components/SubscribeContent.tsx +40 -11
  56. package/apps/dashboard/components/TestTree.tsx +1 -2
  57. package/apps/dashboard/components/TipCard.tsx +2 -4
  58. package/apps/dashboard/components/Toast.tsx +0 -1
  59. package/apps/dashboard/components/TypeIcon.tsx +7 -8
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
  62. package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
  63. package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
  64. package/apps/dashboard/components/WorkItemTree.tsx +2 -4
  65. package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
  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 +20 -73
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
  72. package/apps/dashboard/components/ui/Button.tsx +1 -1
  73. package/apps/dashboard/components/ui/Input.tsx +1 -1
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
  77. package/apps/dashboard/contexts/UsageContext.tsx +62 -31
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  81. package/apps/dashboard/index.html +73 -0
  82. package/apps/dashboard/lib/data-bridge.ts +722 -0
  83. package/apps/dashboard/lib/db.ts +69 -1302
  84. package/apps/dashboard/lib/environment-config.ts +173 -0
  85. package/apps/dashboard/lib/environment-verification.ts +119 -0
  86. package/apps/dashboard/lib/kanban-utils.ts +226 -26
  87. package/apps/dashboard/lib/proof-run.ts +495 -0
  88. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  89. package/apps/dashboard/lib/service-recovery.ts +326 -0
  90. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  91. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  92. package/apps/dashboard/lib/session-stream-manager.ts +253 -122
  93. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  94. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  95. package/apps/dashboard/lib/tauri.ts +106 -0
  96. package/apps/dashboard/lib/utils.ts +3 -3
  97. package/apps/dashboard/next-env.d.ts +1 -1
  98. package/apps/dashboard/package.json +21 -33
  99. package/apps/dashboard/public/bug-icon.png +0 -0
  100. package/apps/dashboard/public/buoy-icon.png +0 -0
  101. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  102. package/apps/dashboard/public/pier-icon.png +0 -0
  103. package/apps/dashboard/public/star-icon.png +0 -0
  104. package/apps/dashboard/public/wrench-icon.png +0 -0
  105. package/apps/dashboard/scripts/tauri-build.js +228 -0
  106. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  107. package/apps/dashboard/src/main.tsx +12 -0
  108. package/apps/dashboard/src/router.tsx +107 -0
  109. package/apps/dashboard/src/vite-env.d.ts +1 -0
  110. package/apps/dashboard/tsconfig.json +7 -12
  111. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  112. package/apps/dashboard/vite.config.ts +33 -0
  113. package/apps/update-server/src/index.ts +167 -30
  114. package/claude-hooks/global-guardrails.js +14 -13
  115. package/crates/jettypod-cli/Cargo.toml +19 -0
  116. package/crates/jettypod-cli/src/commands.rs +1249 -0
  117. package/crates/jettypod-cli/src/main.rs +595 -0
  118. package/crates/jettypod-core/Cargo.toml +26 -0
  119. package/crates/jettypod-core/build.rs +98 -0
  120. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  121. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  122. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  123. package/crates/jettypod-core/src/auth.rs +294 -0
  124. package/crates/jettypod-core/src/config.rs +397 -0
  125. package/crates/jettypod-core/src/db/mod.rs +507 -0
  126. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  127. package/crates/jettypod-core/src/db/startup.rs +101 -0
  128. package/crates/jettypod-core/src/db/validate.rs +149 -0
  129. package/crates/jettypod-core/src/error.rs +76 -0
  130. package/crates/jettypod-core/src/git.rs +458 -0
  131. package/crates/jettypod-core/src/lib.rs +20 -0
  132. package/crates/jettypod-core/src/sessions.rs +625 -0
  133. package/crates/jettypod-core/src/skills.rs +556 -0
  134. package/crates/jettypod-core/src/work.rs +1086 -0
  135. package/crates/jettypod-core/src/worktree.rs +628 -0
  136. package/crates/jettypod-core/src/ws.rs +767 -0
  137. package/cucumber-test.cjs +6 -0
  138. package/jettypod.js +96 -4
  139. package/lib/bdd-preflight.js +96 -0
  140. package/lib/merge-lock.js +111 -253
  141. package/lib/migrations/030-rejection-round-columns.js +54 -0
  142. package/lib/migrations/031-session-isolation-index.js +17 -0
  143. package/lib/work-commands/index.js +58 -16
  144. package/lib/work-tracking/index.js +108 -8
  145. package/package.json +1 -1
  146. package/skills-templates/bug-mode/SKILL.md +43 -1
  147. package/skills-templates/chore-mode/SKILL.md +40 -1
  148. package/skills-templates/design-system-selection/SKILL.md +273 -0
  149. package/skills-templates/epic-planning/SKILL.md +14 -0
  150. package/skills-templates/feature-planning/SKILL.md +90 -1
  151. package/skills-templates/production-mode/SKILL.md +20 -0
  152. package/skills-templates/simple-improvement/SKILL.md +39 -2
  153. package/skills-templates/speed-mode/SKILL.md +10 -15
  154. package/skills-templates/stable-mode/SKILL.md +47 -0
  155. package/apps/dashboard/README.md +0 -36
  156. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
  157. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  158. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
  159. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  160. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -525
  161. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  162. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  163. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  164. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  165. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  166. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  167. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  168. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  169. package/apps/dashboard/app/api/tests/route.ts +0 -9
  170. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  171. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  172. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  173. package/apps/dashboard/app/api/usage/route.ts +0 -17
  174. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  175. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  176. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  177. package/apps/dashboard/app/api/work/[id]/route.ts +0 -35
  178. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
  179. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  180. package/apps/dashboard/app/layout.tsx +0 -55
  181. package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
  182. package/apps/dashboard/electron/ipc-handlers.js +0 -1026
  183. package/apps/dashboard/electron/main.js +0 -2306
  184. package/apps/dashboard/electron/preload.js +0 -125
  185. package/apps/dashboard/electron/session-manager.js +0 -163
  186. package/apps/dashboard/electron-builder.config.js +0 -357
  187. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  188. package/apps/dashboard/lib/backlog-parser.ts +0 -50
  189. package/apps/dashboard/lib/claude-process-manager.ts +0 -529
  190. package/apps/dashboard/lib/db-bridge.ts +0 -283
  191. package/apps/dashboard/lib/prototypes.ts +0 -202
  192. package/apps/dashboard/lib/test-results-db.ts +0 -307
  193. package/apps/dashboard/lib/tests.ts +0 -282
  194. package/apps/dashboard/next.config.js +0 -66
  195. package/apps/dashboard/postcss.config.mjs +0 -7
  196. package/apps/dashboard/public/bug-icon.svg +0 -9
  197. package/apps/dashboard/public/buoy-icon.svg +0 -9
  198. package/apps/dashboard/public/file.svg +0 -1
  199. package/apps/dashboard/public/globe.svg +0 -1
  200. package/apps/dashboard/public/in-flight-seagull.svg +0 -9
  201. package/apps/dashboard/public/next.svg +0 -1
  202. package/apps/dashboard/public/pier-icon.svg +0 -14
  203. package/apps/dashboard/public/star-icon.svg +0 -9
  204. package/apps/dashboard/public/vercel.svg +0 -1
  205. package/apps/dashboard/public/window.svg +0 -1
  206. package/apps/dashboard/public/wrench-icon.svg +0 -9
  207. package/apps/dashboard/scripts/download-node.js +0 -104
  208. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
@@ -1,7 +1,7 @@
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
 
@@ -20,17 +20,18 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
20
20
  const [userPlan, setUserPlan] = useState<string | null>(null);
21
21
  const [pollingPlan, setPollingPlan] = useState<PlanType | null>(null);
22
22
  const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
23
+ const abortRef = useRef<AbortController | null>(null);
23
24
 
24
25
  useEffect(() => {
25
26
  async function loadAuth() {
26
- if (!window.electronAPI?.isElectron) return;
27
- const status = await window.electronAPI.auth.getStatus();
27
+ if (!isTauri()) return;
28
+ const status = await auth.getStatus();
28
29
  if (status.authenticated && status.user) {
29
30
  setUserEmail(status.user.email);
30
31
  setUserPlan(status.user.plan || 'free');
31
32
 
32
33
  // Check server-side plan in case webhook updated it after local JWT was issued
33
- const token = await window.electronAPI.auth.getToken();
34
+ const token = await auth.getToken();
34
35
  if (token) {
35
36
  try {
36
37
  const res = await fetch(`${API_BASE}/auth/me`, {
@@ -40,7 +41,7 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
40
41
  const data = await res.json() as { user: { plan: string; email: string }; token?: string };
41
42
  if (data.user.plan !== 'free') {
42
43
  if (data.token) {
43
- await window.electronAPI.auth.saveToken(data.token, data.user);
44
+ await auth.saveToken(data.token, data.user);
44
45
  }
45
46
  setUserPlan(data.user.plan);
46
47
  setPageState('upgraded');
@@ -59,19 +60,22 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
59
60
  useEffect(() => {
60
61
  return () => {
61
62
  if (pollingRef.current) clearInterval(pollingRef.current);
63
+ abortRef.current?.abort();
62
64
  };
63
65
  }, []);
64
66
 
65
67
  const startPolling = () => {
66
68
  setPageState('polling');
69
+ abortRef.current = new AbortController();
67
70
 
68
71
  pollingRef.current = setInterval(async () => {
69
72
  try {
70
- const token = await window.electronAPI!.auth.getToken();
73
+ const token = await auth.getToken();
71
74
  if (!token) return;
72
75
 
73
76
  const res = await fetch(`${API_BASE}/auth/me`, {
74
77
  headers: { 'Authorization': `Bearer ${token}` },
78
+ signal: abortRef.current?.signal,
75
79
  });
76
80
 
77
81
  if (!res.ok) return;
@@ -80,7 +84,7 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
80
84
 
81
85
  if (data.user.plan !== 'free') {
82
86
  if (data.token) {
83
- await window.electronAPI!.auth.saveToken(data.token, data.user);
87
+ await auth.saveToken(data.token, data.user);
84
88
  }
85
89
  setUserPlan(data.user.plan);
86
90
  setPageState('upgraded');
@@ -93,13 +97,38 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
93
97
  };
94
98
 
95
99
  const handleCheckout = async (plan: 'monthly' | 'lifetime') => {
96
- if (!window.electronAPI?.isElectron) return;
100
+ if (!isTauri()) return;
97
101
  setCheckoutPlan(plan);
98
102
  setError(null);
99
103
 
100
- const result = await window.electronAPI.subscription.createCheckout(plan);
101
- if (!result.success) {
102
- 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.');
103
132
  setCheckoutPlan(null);
104
133
  return;
105
134
  }
@@ -1,7 +1,6 @@
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';
5
4
  import { TypeIcon } from './TypeIcon';
6
5
 
7
6
  const statusIcons: Record<string, string> = {
@@ -1,10 +1,8 @@
1
- 'use client';
2
-
3
1
  import { useState, useEffect } from 'react';
4
2
  import { m, AnimatePresence, useReducedMotion } from 'framer-motion';
5
- import dynamic from 'next/dynamic';
3
+ import { lazy, Suspense } from 'react';
6
4
 
7
- const LazyMarkdown = dynamic(() => import('./LazyMarkdown'), { ssr: false });
5
+ const LazyMarkdown = lazy(() => import('./LazyMarkdown'));
8
6
 
9
7
  const STORAGE_KEY = 'jettypod-dismissed-tips';
10
8
 
@@ -1,4 +1,3 @@
1
- 'use client';
2
1
 
3
2
  import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
4
3
  import { AnimatePresence, m } from 'framer-motion';
@@ -1,19 +1,18 @@
1
- 'use client';
2
1
 
3
2
  import { useState, useRef, useCallback, useEffect } from 'react';
4
3
  import { createPortal } from 'react-dom';
5
4
  import { TYPE_ICONS } from '@/lib/constants';
6
5
  import { shadow } from '@/lib/shadows';
7
6
 
8
- const SVG_ICONS: Record<string, { src: string; label: string }> = {
9
- bug: { src: '/bug-icon.svg', label: 'Bug' },
10
- chore: { src: '/wrench-icon.svg', label: 'Chore' },
11
- epic: { src: '/buoy-icon.svg', label: 'Epic' },
12
- feature: { src: '/star-icon.svg', label: 'Feature' },
7
+ const TYPE_ICON_SRCS: Record<string, { src: string; label: string }> = {
8
+ bug: { src: '/bug-icon.png', label: 'Bug' },
9
+ chore: { src: '/wrench-icon.png', label: 'Chore' },
10
+ epic: { src: '/buoy-icon.png', label: 'Epic' },
11
+ feature: { src: '/star-icon.png', label: 'Feature' },
13
12
  };
14
13
 
15
14
  export function TypeIcon({ type, className }: { type: string; className?: string }) {
16
- const svg = SVG_ICONS[type];
15
+ const svg = TYPE_ICON_SRCS[type];
17
16
  const [tooltip, setTooltip] = useState<{ x: number; y: number } | null>(null);
18
17
  const [mounted, setMounted] = useState(false);
19
18
  const ref = useRef<HTMLSpanElement>(null);
@@ -37,7 +36,7 @@ export function TypeIcon({ type, className }: { type: string; className?: string
37
36
  if (svg) {
38
37
  return (
39
38
  <span ref={ref} className="inline-flex" onMouseEnter={showTooltip} onMouseLeave={hideTooltip}>
40
- <img src={svg.src} alt="" className={className || "w-8 h-8"} />
39
+ <img src={svg.src} alt="" className={`object-contain ${className || "w-6 h-6"}`} />
41
40
  {mounted && tooltip && portalRoot && createPortal(
42
41
  <span
43
42
  className="pointer-events-none fixed"
@@ -0,0 +1,104 @@
1
+
2
+ import { useCallback } from 'react';
3
+
4
+ type ViewMode = 'summary' | 'detail' | 'raw';
5
+
6
+ const READOUT_FILTERS = [
7
+ { id: 'init', label: 'Init', types: ['system'] },
8
+ { id: 'streaming', label: 'Streaming', types: ['content_block_start', 'content_block_delta', 'content_block_stop', 'message_start', 'message_delta', 'message_stop'] },
9
+ { id: 'messages', label: 'Messages', types: ['assistant'] },
10
+ { id: 'tools', label: 'Tools', types: ['user'] },
11
+ { id: 'completion', label: 'Completion', types: ['result', 'done'] },
12
+ { id: 'errors', label: 'Errors', types: ['error'] },
13
+ ] as const;
14
+
15
+ type ReadoutFilterId = typeof READOUT_FILTERS[number]['id'];
16
+
17
+ interface ViewModeToolbarProps {
18
+ viewMode: ViewMode;
19
+ onViewModeChange: (mode: ViewMode) => void;
20
+ hasIntermediates: boolean;
21
+ allExpanded: boolean;
22
+ onToggleExpandAll: () => void;
23
+ activeFilters: Set<ReadoutFilterId>;
24
+ onToggleFilter: (id: ReadoutFilterId) => void;
25
+ }
26
+
27
+ const MODE_LABELS: { mode: ViewMode; label: string }[] = [
28
+ { mode: 'summary', label: 'Summary' },
29
+ { mode: 'detail', label: 'Detail' },
30
+ { mode: 'raw', label: 'Raw' },
31
+ ];
32
+
33
+ export function ViewModeToolbar({
34
+ viewMode,
35
+ onViewModeChange,
36
+ hasIntermediates,
37
+ allExpanded,
38
+ onToggleExpandAll,
39
+ activeFilters,
40
+ onToggleFilter,
41
+ }: ViewModeToolbarProps) {
42
+ return (
43
+ <div className="px-5 py-2 border-b border-zinc-100 flex-shrink-0" data-testid="view-mode-toolbar">
44
+ <div className="flex items-center justify-between">
45
+ {/* Left: mode toggle */}
46
+ <div className="flex items-center gap-1" data-testid="mode-toggle">
47
+ {MODE_LABELS.map(({ mode, label }, i) => (
48
+ <span key={mode} className="flex items-center">
49
+ {i > 0 && <span className="text-zinc-300 mx-1 text-xs select-none">|</span>}
50
+ <button
51
+ onClick={() => onViewModeChange(mode)}
52
+ className={`text-xs cursor-pointer transition-colors duration-200 ease-out ${
53
+ viewMode === mode
54
+ ? 'text-zinc-900 font-semibold underline underline-offset-4 decoration-zinc-900'
55
+ : 'text-zinc-400 hover:text-zinc-600'
56
+ }`}
57
+ data-testid={`mode-${mode}`}
58
+ aria-pressed={viewMode === mode}
59
+ >
60
+ {label}
61
+ </button>
62
+ </span>
63
+ ))}
64
+ </div>
65
+
66
+ {/* Right: contextual action (non-raw modes) */}
67
+ {viewMode === 'detail' && hasIntermediates && (
68
+ <button
69
+ onClick={onToggleExpandAll}
70
+ className="text-xs text-zinc-400 hover:text-zinc-600 cursor-pointer transition-colors duration-200 ease-out"
71
+ data-testid="expand-collapse-all"
72
+ >
73
+ {allExpanded ? 'Collapse all' : 'Expand all'}
74
+ </button>
75
+ )}
76
+ </div>
77
+
78
+ {/* Raw filter chips: own row, centered */}
79
+ {viewMode === 'raw' && (
80
+ <div className="flex justify-center mt-2" data-testid="readout-filter-chips">
81
+ <div className="flex gap-1.5 flex-wrap justify-center">
82
+ {READOUT_FILTERS.map(f => (
83
+ <button
84
+ key={f.id}
85
+ onClick={() => onToggleFilter(f.id)}
86
+ className={`text-xs px-2 py-0.5 rounded-full cursor-pointer transition-colors duration-200 ease-out ${
87
+ activeFilters.has(f.id)
88
+ ? 'bg-purple-100 text-purple-700'
89
+ : 'bg-zinc-100 text-zinc-400'
90
+ }`}
91
+ data-testid={`readout-filter-${f.id}`}
92
+ >
93
+ {f.label}
94
+ </button>
95
+ ))}
96
+ </div>
97
+ </div>
98
+ )}
99
+ </div>
100
+ );
101
+ }
102
+
103
+ export type { ViewMode, ReadoutFilterId, ViewModeToolbarProps };
104
+ export { READOUT_FILTERS };
@@ -1,4 +1,3 @@
1
- 'use client';
2
1
 
3
2
  import { useState, useEffect, useRef, ReactNode } from 'react';
4
3
 
@@ -16,7 +15,6 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
16
15
  const containerRef = useRef<HTMLDivElement>(null);
17
16
  const hasStartedRef = useRef(false);
18
17
  const timeoutRefs = useRef<NodeJS.Timeout[]>([]);
19
- const measuredHeightRef = useRef<number | null>(null);
20
18
 
21
19
  const clearAllTimeouts = () => {
22
20
  timeoutRefs.current.forEach(clearTimeout);
@@ -35,8 +33,7 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
35
33
  if (isPlaying && !hasStartedRef.current) {
36
34
  hasStartedRef.current = true;
37
35
 
38
- const prefersReducedMotion = typeof window !== 'undefined' &&
39
- window.matchMedia('(prefers-reduced-motion: reduce)').matches;
36
+ const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
40
37
 
41
38
  if (prefersReducedMotion) {
42
39
  setPhase('complete');
@@ -44,12 +41,6 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
44
41
  return;
45
42
  }
46
43
 
47
- // Capture height before animation for smooth collapse later
48
- if (containerRef.current) {
49
- measuredHeightRef.current = containerRef.current.offsetHeight;
50
- containerRef.current.style.maxHeight = `${measuredHeightRef.current}px`;
51
- }
52
-
53
44
  // Phase 1: Content disappears instantly, video starts playing
54
45
  setPhase('video-playing');
55
46
 
@@ -80,10 +71,6 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
80
71
  clearAllTimeouts();
81
72
  hasStartedRef.current = false;
82
73
  setPhase('idle');
83
- measuredHeightRef.current = null;
84
- if (containerRef.current) {
85
- containerRef.current.style.maxHeight = '';
86
- }
87
74
  }
88
75
  }, [isPlaying, phase]);
89
76
 
@@ -100,9 +87,10 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
100
87
  style={{
101
88
  overflow: 'hidden',
102
89
  opacity: isCollapsing ? 0 : 1,
103
- maxHeight: isCollapsing ? 0 : undefined,
90
+ transform: isCollapsing ? 'scaleY(0)' : 'scaleY(1)',
91
+ transformOrigin: 'top',
104
92
  transition: isCollapsing
105
- ? 'opacity 0.4s ease-out, max-height 0.5s ease-out'
93
+ ? 'opacity 0.4s ease-out, transform 0.5s ease-out'
106
94
  : undefined,
107
95
  }}
108
96
  >
@@ -118,7 +106,7 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
118
106
  }}
119
107
  muted
120
108
  playsInline
121
- preload="auto"
109
+ preload="none"
122
110
  src="/assets/wave-completion.mp4"
123
111
  onError={handleVideoError}
124
112
  />
@@ -1,8 +1,5 @@
1
- 'use client';
2
-
3
- import Image from 'next/image';
4
1
  import { Button } from '@/components/ui/Button';
5
- import type { RecentProject } from '@/lib/db-bridge';
2
+ import type { RecentProject } from '@/lib/tauri-bridge';
6
3
 
7
4
  interface WelcomeScreenProps {
8
5
  recentProjects?: RecentProject[];
@@ -22,12 +19,11 @@ export function WelcomeScreen({
22
19
  <div className="max-w-md w-full space-y-6">
23
20
  {/* Logo */}
24
21
  <div className="flex flex-col items-center space-y-4">
25
- <Image
22
+ <img
26
23
  src="/jettypod_wordmark.png"
27
24
  alt="JettyPod"
28
25
  width={160}
29
26
  height={40}
30
- priority
31
27
  />
32
28
  <p className="text-zinc-500 dark:text-zinc-400 text-center">
33
29
  Select a project to get started
@@ -1,4 +1,3 @@
1
- 'use client';
2
1
 
3
2
  import { CopyableId } from './CopyableId';
4
3
  import { TypeIcon } from './TypeIcon';
@@ -1,7 +1,5 @@
1
- 'use client';
2
-
3
1
  import { useState } from 'react';
4
- import Link from 'next/link';
2
+ import { Link } from 'react-router-dom';
5
3
  import type { WorkItem } from '@/lib/db';
6
4
  import { CopyableId } from './CopyableId';
7
5
  import { STATUS_COLORS, MODE_LABELS } from '@/lib/constants';
@@ -42,7 +40,7 @@ function WorkItemNode({ item, depth = 0 }: WorkItemNodeProps) {
42
40
 
43
41
  {/* Title */}
44
42
  <Link
45
- href={`/work/${item.id}`}
43
+ to={`/work/${item.id}`}
46
44
  className={`flex-1 truncate hover:underline ${STATUS_COLORS[item.status]}`}
47
45
  >
48
46
  {item.title}
@@ -1,10 +1,11 @@
1
- 'use client';
2
-
3
1
  import { useState, useEffect } from 'react';
4
- import { useRouter } from 'next/navigation';
2
+ import { useNavigate } from 'react-router-dom';
5
3
  import { useUsage } from '@/contexts/UsageContext';
6
4
  import { SubscribeContent } from '@/components/SubscribeContent';
7
5
  import { Button } from '@/components/ui/Button';
6
+ import { isTauri, auth, shell } from '@/lib/tauri-bridge';
7
+
8
+ const API_BASE = 'https://jettypod-update-server.spangbaryn2.workers.dev';
8
9
 
9
10
  const planColors: Record<string, string> = {
10
11
  free: 'bg-zinc-200 dark:bg-zinc-700 text-zinc-700 dark:text-zinc-300',
@@ -13,7 +14,7 @@ const planColors: Record<string, string> = {
13
14
  };
14
15
 
15
16
  export function AccountSection() {
16
- const router = useRouter();
17
+ const navigate = useNavigate();
17
18
  const { plan, used, limit, loading, refresh } = useUsage();
18
19
  const [email, setEmail] = useState<string | null>(null);
19
20
  const [portalError, setPortalError] = useState<string | null>(null);
@@ -22,9 +23,9 @@ export function AccountSection() {
22
23
 
23
24
  useEffect(() => {
24
25
  async function loadAuth() {
25
- if (!window.electronAPI?.isElectron) return;
26
+ if (!isTauri()) return;
26
27
  try {
27
- const status = await window.electronAPI.auth.getStatus();
28
+ const status = await auth.getStatus();
28
29
  if (status.authenticated && status.user) {
29
30
  setEmail(status.user.email);
30
31
  }
@@ -39,13 +40,27 @@ export function AccountSection() {
39
40
  const isAtCapacity = isFree && limit > 0 && used >= limit;
40
41
 
41
42
  const handleManageSubscription = async () => {
42
- if (!window.electronAPI?.billing?.openCustomerPortal) return;
43
43
  setPortalError(null);
44
44
  setPortalLoading(true);
45
45
  try {
46
- const result = await window.electronAPI.billing.openCustomerPortal();
47
- if (result && !result.success) {
48
- setPortalError(result.error || 'Unable to open billing portal');
46
+ const token = await auth.getToken();
47
+ if (!token) {
48
+ setPortalError('Not authenticated');
49
+ return;
50
+ }
51
+ const res = await fetch(`${API_BASE}/billing/customer-portal`, {
52
+ method: 'POST',
53
+ headers: { 'Authorization': `Bearer ${token}` },
54
+ });
55
+ if (res.ok) {
56
+ const data = await res.json() as { url?: string };
57
+ if (data.url) {
58
+ await shell.openUrl(data.url);
59
+ } else {
60
+ setPortalError('Unable to open billing portal');
61
+ }
62
+ } else {
63
+ setPortalError('Unable to open billing portal');
49
64
  }
50
65
  } catch {
51
66
  setPortalError('Unable to open billing portal');
@@ -55,9 +70,8 @@ export function AccountSection() {
55
70
  };
56
71
 
57
72
  const handleLogout = async () => {
58
- if (!window.electronAPI?.auth?.logout) return;
59
- await window.electronAPI.auth.logout();
60
- router.push('/login');
73
+ await auth.logout();
74
+ navigate('/login');
61
75
  };
62
76
 
63
77
  const handleUpgradeClose = () => {
@@ -0,0 +1,89 @@
1
+ import { useState } from 'react';
2
+ import { invoke } from '@/lib/tauri';
3
+ import { openDialog } from '@/lib/tauri';
4
+ import { Button } from '@/components/ui/Button';
5
+ import { ContextDocumentsSection } from './ContextDocumentsSection';
6
+ import type { ContextDocument } from '@/lib/data-bridge';
7
+
8
+ interface AiContextSectionProps {
9
+ initialDesignSystemDir: string | null;
10
+ initialContextDocuments: ContextDocument[];
11
+ }
12
+
13
+ export function AiContextSection({ initialDesignSystemDir, initialContextDocuments }: AiContextSectionProps) {
14
+ const [designSystemDir, setDesignSystemDir] = useState<string | null>(initialDesignSystemDir);
15
+ const [successMessage, setSuccessMessage] = useState<string | null>(null);
16
+
17
+ const showSuccess = (message: string) => {
18
+ setSuccessMessage(message);
19
+ setTimeout(() => setSuccessMessage(null), 3000);
20
+ };
21
+
22
+ const handleBrowse = async () => {
23
+ const selected = await openDialog({ directory: true, title: 'Select Design System Directory' });
24
+ if (selected && typeof selected === 'string') {
25
+ await invoke('db_set_design_system_dir', { dir: selected });
26
+ setDesignSystemDir(selected);
27
+ showSuccess('Design system directory set');
28
+ }
29
+ };
30
+
31
+ const handleClear = async () => {
32
+ await invoke('db_reset_design_system_dir');
33
+ setDesignSystemDir(null);
34
+ showSuccess('Design system directory cleared');
35
+ };
36
+
37
+ return (
38
+ <section id="ai-context">
39
+ <div className="flex items-center justify-between mb-6">
40
+ <h2 className="text-lg font-medium text-zinc-900 dark:text-zinc-100">
41
+ AI Context
42
+ </h2>
43
+ </div>
44
+
45
+ {successMessage && (
46
+ <div className="mb-4 px-6 py-3 bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 rounded-lg text-base">
47
+ {successMessage}
48
+ </div>
49
+ )}
50
+
51
+ {/* Design System Directory Setting */}
52
+ <div className="p-6 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg">
53
+ <div>
54
+ <label className="block text-base font-medium text-zinc-900 dark:text-zinc-100">
55
+ Design System Directory
56
+ </label>
57
+ <p className="text-base text-zinc-500 dark:text-zinc-400 mt-1">
58
+ Select a folder that includes your design system. Claude will use this when prototyping and building parts of your project.
59
+ </p>
60
+ </div>
61
+
62
+ <div className="mt-4">
63
+ {designSystemDir ? (
64
+ <div className="flex items-center gap-3">
65
+ <span className="font-mono text-sm text-zinc-900 dark:text-zinc-100 bg-zinc-200 dark:bg-zinc-700 px-3 py-2 rounded flex-1 truncate">
66
+ {designSystemDir}
67
+ </span>
68
+ <Button onClick={handleBrowse} variant="ghost" size="sm">
69
+ Change
70
+ </Button>
71
+ <Button onClick={handleClear} variant="ghost" size="sm">
72
+ Clear
73
+ </Button>
74
+ </div>
75
+ ) : (
76
+ <div className="flex items-center gap-3">
77
+ <Button onClick={handleBrowse} size="sm">
78
+ Browse...
79
+ </Button>
80
+ </div>
81
+ )}
82
+ </div>
83
+ </div>
84
+
85
+ {/* Context Documents */}
86
+ <ContextDocumentsSection initialDocuments={initialContextDocuments} />
87
+ </section>
88
+ );
89
+ }