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,27 +1,75 @@
1
- import { getEnvVars, discoverEnvFiles, getSelectedEnvFile, getMainBranch } from '@/lib/db';
1
+ import { useState, useEffect } from 'react';
2
2
  import { AccountSection } from '@/components/settings/AccountSection';
3
3
  import { EnvVarsSection } from '@/components/settings/EnvVarsSection';
4
4
  import { GeneralSection } from '@/components/settings/GeneralSection';
5
+ import { AiContextSection } from '@/components/settings/AiContextSection';
6
+ import { ProjectStackSection } from '@/components/settings/ProjectStackSection';
5
7
  import { SettingsLayout } from '@/components/settings/SettingsLayout';
6
-
7
- export const dynamic = 'force-dynamic';
8
+ import { dataBridge, prefetch } from '@/lib/data-bridge';
9
+ import type { ContextDocument } from '@/lib/data-bridge';
10
+ import type { EnvironmentConfig } from '@/lib/environment-config';
8
11
 
9
12
  export default function SettingsPage() {
10
- const envFiles = discoverEnvFiles();
11
- const selectedFile = getSelectedEnvFile() || (envFiles.includes('.env') ? '.env' : envFiles[0] || null);
12
- const envVars = selectedFile ? getEnvVars(selectedFile) : [];
13
- const mainBranch = getMainBranch();
13
+ const [envFiles, setEnvFiles] = useState<string[]>([]);
14
+ const [selectedFile, setSelectedFile] = useState<string | null>(null);
15
+ const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>([]);
16
+ const [mainBranch, setMainBranch] = useState('main');
17
+ const [claudeModel, setClaudeModel] = useState<string | null>(null);
18
+ const [designSystemDir, setDesignSystemDir] = useState<string | null>(null);
19
+ const [contextDocuments, setContextDocuments] = useState<ContextDocument[]>([]);
20
+ const [environmentConfig, setEnvironmentConfig] = useState<EnvironmentConfig | null>(null);
21
+ const [loading, setLoading] = useState(true);
22
+
23
+ useEffect(() => {
24
+ async function loadSettings() {
25
+ try {
26
+ const { files, selected, branch, claudeModel: model, designSystemDir: dsDir, contextDocuments: ctxDocs, environmentConfig: envConfig } = await prefetch.settings();
27
+ setEnvFiles(files);
28
+ const activeFile = selected || (files.includes('.env') ? '.env' : files[0] || null);
29
+ setSelectedFile(activeFile);
30
+ if (activeFile) {
31
+ const vars = await dataBridge.getEnvVars(activeFile);
32
+ setEnvVars(vars);
33
+ }
34
+ setMainBranch(branch);
35
+ setClaudeModel(model);
36
+ setDesignSystemDir(dsDir);
37
+ setContextDocuments(ctxDocs);
38
+ setEnvironmentConfig(envConfig);
39
+ } catch (err) {
40
+ console.error('Failed to load settings:', err);
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ }
45
+ loadSettings();
46
+ }, []);
47
+
48
+ if (loading) return (
49
+ <div className="flex-1 overflow-auto max-w-7xl w-full mx-auto px-4 py-4">
50
+ <div className="h-8 w-40 bg-zinc-200 dark:bg-zinc-800 rounded mb-8" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
51
+ <div className="flex gap-4 mb-6">
52
+ {[1, 2, 3, 4, 5].map(i => (
53
+ <div key={i} className="h-9 w-32 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
54
+ ))}
55
+ </div>
56
+ <div className="space-y-4">
57
+ <div className="h-12 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
58
+ <div className="h-12 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
59
+ </div>
60
+ </div>
61
+ );
14
62
 
15
63
  return (
16
64
  <div className="flex-1 overflow-auto max-w-7xl w-full mx-auto px-4 py-4">
17
- <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-8">
18
- Settings
19
- </h1>
65
+ <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-8">Settings</h1>
20
66
  <SettingsLayout
21
67
  tabs={[
22
68
  { id: 'account', label: 'Account', content: <AccountSection /> },
23
- { id: 'general', label: 'General', content: <GeneralSection initialMainBranch={mainBranch} /> },
24
- { id: 'env-vars', label: 'Environment Variables', content: <EnvVarsSection initialEnvVars={envVars} envFiles={envFiles} selectedFile={selectedFile} /> },
69
+ { id: 'general', label: 'General', content: <GeneralSection initialMainBranch={{ branch: mainBranch, source: 'detected' }} initialClaudeModel={claudeModel} /> },
70
+ { id: 'env-vars', label: 'Environment Variables', content: <EnvVarsSection initialEnvVars={envVars.map(v => ({ name: v.key, value: v.value }))} envFiles={envFiles} selectedFile={selectedFile} /> },
71
+ { id: 'ai-context', label: 'AI Context', content: <AiContextSection initialDesignSystemDir={designSystemDir} initialContextDocuments={contextDocuments} /> },
72
+ { id: 'project-stack', label: 'Your Project Stack', content: <ProjectStackSection initialConfig={environmentConfig} /> },
25
73
  ]}
26
74
  />
27
75
  </div>
@@ -1,10 +1,8 @@
1
- 'use client';
2
-
3
1
  import { useState, useEffect, useRef } from 'react';
4
- import Image from 'next/image';
5
- import Link from 'next/link';
2
+ import { Link } from 'react-router-dom';
6
3
  import { Button } from '@/components/ui/Button';
7
4
  import { Input } from '@/components/ui/Input';
5
+ import { isTauri, auth } from '@/lib/tauri-bridge';
8
6
 
9
7
  const API_BASE = 'https://jettypod-update-server.spangbaryn2.workers.dev';
10
8
 
@@ -21,11 +19,11 @@ export default function SignupPage() {
21
19
  // Redirect already-authenticated users to dashboard
22
20
  useEffect(() => {
23
21
  async function checkIfAlreadyAuthenticated() {
24
- if (window.electronAPI?.isElectron) {
22
+ if (isTauri()) {
25
23
  try {
26
- const status = await window.electronAPI.auth.getStatus();
24
+ const status = await auth.getStatus();
27
25
  if (status.authenticated) {
28
- const path = await window.electronAPI.auth.getPostLoginPath?.() || '/';
26
+ const path = await auth.getPostLoginPath() || '/';
29
27
  window.location.href = path;
30
28
  }
31
29
  } catch {
@@ -43,15 +41,15 @@ export default function SignupPage() {
43
41
  }, []);
44
42
 
45
43
  const handleGoogleSignUp = () => {
46
- if (!window.electronAPI?.isElectron) return;
47
- window.electronAPI.auth.loginWithGoogle();
44
+ if (!isTauri()) return;
45
+ auth.loginWithGoogle();
48
46
 
49
47
  pollRef.current = setInterval(async () => {
50
48
  try {
51
- const status = await window.electronAPI!.auth.getStatus();
49
+ const status = await auth.getStatus();
52
50
  if (status.authenticated) {
53
51
  if (pollRef.current) clearInterval(pollRef.current);
54
- const path = await window.electronAPI!.auth.getPostLoginPath?.() || '/';
52
+ const path = await auth.getPostLoginPath() || '/';
55
53
  window.location.href = path;
56
54
  }
57
55
  } catch {
@@ -119,11 +117,11 @@ export default function SignupPage() {
119
117
 
120
118
  const data = await res.json() as { token: string; user: { id: string; email: string; plan: string } };
121
119
 
122
- if (window.electronAPI?.isElectron) {
123
- await window.electronAPI.auth.saveToken(data.token, data.user);
120
+ if (isTauri()) {
121
+ await auth.saveToken(data.token, data.user);
124
122
  }
125
123
 
126
- const path = await window.electronAPI?.auth.getPostLoginPath?.() || '/';
124
+ const path = await auth.getPostLoginPath() || '/';
127
125
  window.location.href = path;
128
126
  } catch {
129
127
  setError('Failed to verify code. Check your connection.');
@@ -136,12 +134,11 @@ export default function SignupPage() {
136
134
  <div className="max-w-md w-full space-y-10">
137
135
  {/* Logo */}
138
136
  <div className="flex flex-col items-center space-y-6">
139
- <Image
137
+ <img
140
138
  src="/jettypod_wordmark.png"
141
139
  alt="JettyPod"
142
140
  width={160}
143
141
  height={40}
144
- priority
145
142
  />
146
143
  <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
147
144
  Create your account
@@ -235,7 +232,7 @@ export default function SignupPage() {
235
232
  {/* Switch to login */}
236
233
  <p className="text-center text-base text-zinc-500 dark:text-zinc-400">
237
234
  Already have an account?{' '}
238
- <Link href="/login" className="font-medium hover:underline" style={{ color: '#819D9F' }}>
235
+ <Link to="/login" className="font-medium hover:underline" style={{ color: '#819D9F' }}>
239
236
  Sign in
240
237
  </Link>
241
238
  </p>
@@ -1,5 +1,3 @@
1
- 'use client';
2
-
3
1
  import { SubscribeContent } from '@/components/SubscribeContent';
4
2
 
5
3
  export default function SubscribePage() {
@@ -1,10 +1,43 @@
1
- import { getTestDashboardData } from '@/lib/tests';
1
+ import { useState, useEffect } from 'react';
2
2
  import { RealTimeTestsWrapper } from '@/components/RealTimeTestsWrapper';
3
-
4
- export const dynamic = 'force-dynamic';
3
+ import { prefetch } from '@/lib/data-bridge';
5
4
 
6
5
  export default function TestsPage() {
7
- const initialData = getTestDashboardData();
6
+ const [initialData, setInitialData] = useState<any>(null);
7
+ const [loading, setLoading] = useState(true);
8
+
9
+ useEffect(() => {
10
+ async function loadData() {
11
+ try {
12
+ const data = await prefetch.tests();
13
+ setInitialData(data);
14
+ } catch (err) {
15
+ console.error('Failed to load test data:', err);
16
+ setInitialData({ suites: [], summary: { total: 0, passing: 0, failing: 0, pending: 0 } });
17
+ } finally {
18
+ setLoading(false);
19
+ }
20
+ }
21
+ loadData();
22
+ }, []);
23
+
24
+ if (loading || !initialData) return (
25
+ <div className="flex-1 max-w-7xl w-full mx-auto px-4 py-4">
26
+ <div className="flex justify-between items-center mb-6">
27
+ <div className="h-8 w-32 bg-zinc-200 dark:bg-zinc-800 rounded" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
28
+ <div className="flex gap-3">
29
+ {[1, 2, 3, 4].map(i => (
30
+ <div key={i} className="h-10 w-20 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
31
+ ))}
32
+ </div>
33
+ </div>
34
+ <div className="space-y-3">
35
+ {[1, 2, 3, 4, 5].map(i => (
36
+ <div key={i} className="h-16 bg-zinc-200 dark:bg-zinc-800 rounded-xl" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
37
+ ))}
38
+ </div>
39
+ </div>
40
+ );
8
41
 
9
42
  return <RealTimeTestsWrapper initialData={initialData} />;
10
43
  }
@@ -1,8 +1,7 @@
1
- 'use client';
2
-
3
1
  import { useState, useEffect } from 'react';
4
2
  import { WelcomeScreen } from '@/components/WelcomeScreen';
5
- import type { RecentProject } from '@/lib/db-bridge';
3
+ import { isTauri, project } from '@/lib/tauri-bridge';
4
+ import type { RecentProject } from '@/lib/tauri-bridge';
6
5
 
7
6
  export default function WelcomePage() {
8
7
  const [error, setError] = useState<string | null>(null);
@@ -11,11 +10,11 @@ export default function WelcomePage() {
11
10
  // Load recent projects on mount
12
11
  useEffect(() => {
13
12
  async function loadRecentProjects() {
14
- if (!window.electronAPI?.isElectron) {
13
+ if (!isTauri()) {
15
14
  return;
16
15
  }
17
16
 
18
- const projects = await window.electronAPI.project.getRecent();
17
+ const projects = await project.getRecent();
19
18
  setRecentProjects(projects);
20
19
  }
21
20
 
@@ -25,12 +24,12 @@ export default function WelcomePage() {
25
24
  const handleNewProject = async () => {
26
25
  setError(null);
27
26
 
28
- if (!window.electronAPI?.isElectron) {
27
+ if (!isTauri()) {
29
28
  setError('Project creation is only available in the desktop app.');
30
29
  return;
31
30
  }
32
31
 
33
- const result = await window.electronAPI.project.newProject();
32
+ const result = await project.newProject();
34
33
 
35
34
  if (result.canceled) {
36
35
  return;
@@ -47,13 +46,12 @@ export default function WelcomePage() {
47
46
  const handleOpenProject = async () => {
48
47
  setError(null);
49
48
 
50
- // Check if we're in Electron
51
- if (!window.electronAPI?.isElectron) {
49
+ if (!isTauri()) {
52
50
  setError('Project selection is only available in the desktop app.');
53
51
  return;
54
52
  }
55
53
 
56
- const result = await window.electronAPI.project.openDialog();
54
+ const result = await project.openDialog();
57
55
 
58
56
  if (result.canceled) {
59
57
  // User canceled - do nothing
@@ -69,19 +67,18 @@ export default function WelcomePage() {
69
67
  window.location.href = '/';
70
68
  };
71
69
 
72
- const handleSelectRecentProject = async (project: RecentProject) => {
70
+ const handleSelectRecentProject = async (p: RecentProject) => {
73
71
  setError(null);
74
72
 
75
- // Check if we're in Electron
76
- if (!window.electronAPI?.isElectron) {
73
+ if (!isTauri()) {
77
74
  setError('Project selection is only available in the desktop app.');
78
75
  return;
79
76
  }
80
77
 
81
- const result = await window.electronAPI.project.openRecent(project.path);
78
+ const result = await project.openRecent(p.path);
82
79
 
83
80
  if (!result.success) {
84
- setError(result.error || 'Failed to open project');
81
+ setError('Failed to open project');
85
82
  return;
86
83
  }
87
84
 
@@ -1,74 +1,117 @@
1
- import { getWorkItem, getChildWorkItems, getDecisionsForWorkItem } from '@/lib/db';
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { useParams, Link, useNavigate } from 'react-router-dom';
2
3
  import { WorkItemTree } from '@/components/WorkItemTree';
3
4
  import { WorkItemHeader } from '@/components/WorkItemHeader';
4
5
  import { EditableDetailTitle } from '@/components/EditableDetailTitle';
5
6
  import { EditableDetailDescription } from '@/components/EditableDetailDescription';
6
- import Link from 'next/link';
7
- import { notFound } from 'next/navigation';
8
7
  import { TYPE_LABELS, STATUS_LABELS, MODE_LABELS_FULL } from '@/lib/constants';
9
8
  import { TypeIcon } from '@/components/TypeIcon';
10
9
  import { DetailReviewActions } from '@/components/DetailReviewActions';
10
+ import { dataBridge, prefetch } from '@/lib/data-bridge';
11
+ import type { WorkItemData, DecisionData } from '@/lib/data-bridge';
11
12
 
12
- interface PageProps {
13
- params: Promise<{ id: string }>;
14
- }
13
+ export default function WorkItemPage() {
14
+ const { id } = useParams<{ id: string }>();
15
+ const navigate = useNavigate();
16
+ const [item, setItem] = useState<WorkItemData | null>(null);
17
+ const [children, setChildren] = useState<WorkItemData[]>([]);
18
+ const [decisions, setDecisions] = useState<DecisionData[]>([]);
19
+ const [parentItem, setParentItem] = useState<WorkItemData | null>(null);
20
+ const [loading, setLoading] = useState(true);
21
+ const [notFound, setNotFound] = useState(false);
15
22
 
16
- export default async function WorkItemPage({ params }: PageProps) {
17
- const { id } = await params;
18
- const workItemId = parseInt(id, 10);
23
+ useEffect(() => {
24
+ async function loadData() {
25
+ const workItemId = parseInt(id || '', 10);
26
+ if (isNaN(workItemId)) {
27
+ setNotFound(true);
28
+ setLoading(false);
29
+ return;
30
+ }
19
31
 
20
- if (isNaN(workItemId)) {
21
- notFound();
22
- }
32
+ try {
33
+ // Uses prefetch cache — if the user hovered the kanban card, data is
34
+ // already loaded. Otherwise fetches fresh (same parallel IPC calls).
35
+ const { item: workItem, children: childItems, decisions: decisionItems } =
36
+ await prefetch.workItem(workItemId);
37
+
38
+ if (!workItem) {
39
+ setNotFound(true);
40
+ setLoading(false);
41
+ return;
42
+ }
43
+
44
+ // Parent fetch depends on the work item's parent_id, but we can
45
+ // overlap it with setState calls since React batches them.
46
+ const parent = workItem.parent_id
47
+ ? await dataBridge.getWorkItem(workItem.parent_id)
48
+ : null;
49
+
50
+ setItem(workItem);
51
+ setChildren(childItems);
52
+ setDecisions(decisionItems);
53
+ setParentItem(parent);
54
+ } catch (err) {
55
+ console.error('Failed to load work item:', err);
56
+ setNotFound(true);
57
+ } finally {
58
+ setLoading(false);
59
+ }
60
+ }
61
+ loadData();
62
+ }, [id]);
23
63
 
24
- const item = getWorkItem(workItemId);
64
+ const handleTitleChange = useCallback((newTitle: string) => {
65
+ setItem(prev => prev ? { ...prev, title: newTitle } : prev);
66
+ }, []);
25
67
 
26
- if (!item) {
27
- notFound();
68
+ const handleDescriptionChange = useCallback((newDescription: string) => {
69
+ setItem(prev => prev ? { ...prev, description: newDescription } : prev);
70
+ }, []);
71
+
72
+ if (loading) return null;
73
+
74
+ if (notFound) {
75
+ return (
76
+ <div className="flex-1 flex items-center justify-center">
77
+ <div className="text-center">
78
+ <h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-100 mb-2">Not Found</h1>
79
+ <p className="text-zinc-500 mb-4">Work item not found.</p>
80
+ <Link to="/" viewTransition className="text-[#5a7d7f] hover:underline">← Back to Dashboard</Link>
81
+ </div>
82
+ </div>
83
+ );
28
84
  }
29
85
 
30
- const children = getChildWorkItems(workItemId);
31
- const decisions = getDecisionsForWorkItem(workItemId);
32
- const parentItem = item.parent_id ? getWorkItem(item.parent_id) : null;
86
+ if (!item) return null;
33
87
 
34
88
  const typeInfo = TYPE_LABELS[item.type] || { icon: '📄', label: 'Item' };
35
89
  const statusInfo = STATUS_LABELS[item.status] || STATUS_LABELS.backlog;
36
90
 
37
91
  return (
38
- <div className="min-h-screen bg-zinc-50 dark:bg-zinc-950">
39
- {/* Header */}
92
+ <div className="flex-1 overflow-y-auto bg-zinc-50 dark:bg-zinc-950">
40
93
  <header className="border-b border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900">
41
94
  <div className="max-w-4xl mx-auto px-6 py-6">
42
- <Link href="/" className="text-[#5a7d7f] dark:text-[#a3bfc0] hover:underline text-base">
43
- ← Back to Dashboard
44
- </Link>
95
+ <Link to="/" viewTransition className="text-[#5a7d7f] dark:text-[#a3bfc0] hover:underline text-base">← Back to Dashboard</Link>
45
96
  </div>
46
97
  </header>
47
98
 
48
99
  <main className="max-w-4xl mx-auto px-4 py-6">
49
- {/* Breadcrumb */}
50
100
  {parentItem && (
51
101
  <div className="mb-6 text-base text-zinc-500">
52
- <Link href={`/work/${parentItem.id}`} className="inline-flex items-center gap-1.5 hover:underline">
102
+ <Link to={`/work/${parentItem.id}`} viewTransition className="inline-flex items-center gap-1.5 hover:underline">
53
103
  <TypeIcon type={parentItem.type} className="w-6 h-6" /> #{parentItem.id} {parentItem.title}
54
104
  </Link>
55
105
  <span className="mx-2">→</span>
56
106
  </div>
57
107
  )}
58
108
 
59
- {/* Main card */}
60
109
  <div className="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 overflow-hidden">
61
- {/* Header */}
62
110
  <div className="px-8 py-6 border-b border-zinc-200 dark:border-zinc-800">
63
111
  <div className="flex items-start justify-between gap-6">
64
112
  <div className="flex-1 min-w-0">
65
- <WorkItemHeader
66
- id={item.id}
67
- title={item.title}
68
- type={item.type}
69
- typeLabel={typeInfo.label}
70
- />
71
- <EditableDetailTitle title={item.title} itemId={item.id} />
113
+ <WorkItemHeader id={item.id} title={item.title} type={item.type} typeLabel={typeInfo.label} />
114
+ <EditableDetailTitle title={item.title} itemId={item.id} onTitleChange={handleTitleChange} />
72
115
  </div>
73
116
  <div className="flex items-center gap-2">
74
117
  {item.mode && MODE_LABELS_FULL[item.mode] && (
@@ -76,29 +119,19 @@ export default async function WorkItemPage({ params }: PageProps) {
76
119
  {MODE_LABELS_FULL[item.mode].label}
77
120
  </span>
78
121
  )}
79
- <span className={`text-base px-3 py-1.5 rounded ${statusInfo.color}`}>
80
- {statusInfo.label}
81
- </span>
82
- {!!item.ready_for_review && (
83
- <DetailReviewActions workItemId={item.id} />
84
- )}
122
+ <span className={`text-base px-3 py-1.5 rounded ${statusInfo.color}`}>{statusInfo.label}</span>
123
+ {!!item.ready_for_review && <DetailReviewActions workItemId={item.id} />}
85
124
  </div>
86
125
  </div>
87
126
  </div>
88
127
 
89
- {/* Description */}
90
128
  <div className="px-8 py-6 border-b border-zinc-200 dark:border-zinc-800">
91
- <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-3">
92
- Description
93
- </h2>
94
- <EditableDetailDescription description={item.description} itemId={item.id} />
129
+ <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-3">Description</h2>
130
+ <EditableDetailDescription description={item.description} itemId={item.id} onDescriptionChange={handleDescriptionChange} />
95
131
  </div>
96
132
 
97
- {/* Metadata */}
98
133
  <div className="px-8 py-6 border-b border-zinc-200 dark:border-zinc-800">
99
- <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-4">
100
- Details
101
- </h2>
134
+ <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-4">Details</h2>
102
135
  <dl className="grid grid-cols-2 gap-6 text-base">
103
136
  {item.branch_name && (
104
137
  <div>
@@ -114,59 +147,41 @@ export default async function WorkItemPage({ params }: PageProps) {
114
147
  )}
115
148
  <div>
116
149
  <dt className="text-zinc-500">Created</dt>
117
- <dd className="text-zinc-900 dark:text-zinc-100">
118
- {new Date(item.created_at).toLocaleDateString()}
119
- </dd>
150
+ <dd className="text-zinc-900 dark:text-zinc-100">{new Date(item.created_at).toLocaleDateString()}</dd>
120
151
  </div>
121
152
  {item.completed_at && (
122
153
  <div>
123
154
  <dt className="text-zinc-500">Completed</dt>
124
- <dd className="text-zinc-900 dark:text-zinc-100">
125
- {new Date(item.completed_at).toLocaleDateString()}
126
- </dd>
155
+ <dd className="text-zinc-900 dark:text-zinc-100">{new Date(item.completed_at).toLocaleDateString()}</dd>
127
156
  </div>
128
157
  )}
129
158
  </dl>
130
159
  </div>
131
160
 
132
- {/* Decisions */}
133
161
  {decisions.length > 0 && (
134
162
  <div className="px-8 py-6 border-b border-zinc-200 dark:border-zinc-800">
135
- <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-3">
136
- Decisions
137
- </h2>
163
+ <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-3">Decisions</h2>
138
164
  <div className="space-y-6">
139
165
  {decisions.map((decision) => (
140
166
  <div key={decision.id} className="bg-zinc-50 dark:bg-zinc-800 rounded-lg p-6">
141
167
  <div className="flex items-center gap-3 mb-3">
142
- <span className="text-base font-medium text-zinc-900 dark:text-zinc-100">
143
- {decision.aspect}
144
- </span>
145
- <span className="text-base text-zinc-500">
146
- {new Date(decision.created_at).toLocaleDateString()}
147
- </span>
168
+ <span className="text-base font-medium text-zinc-900 dark:text-zinc-100">{decision.aspect}</span>
169
+ <span className="text-base text-zinc-500">{new Date(decision.created_at).toLocaleDateString()}</span>
148
170
  </div>
149
- <p className="text-zinc-700 dark:text-zinc-300 font-medium">
150
- {decision.decision}
151
- </p>
152
- {decision.rationale && (
153
- <p className="text-base text-zinc-500 mt-1.5">
154
- {decision.rationale}
155
- </p>
156
- )}
171
+ <p className="text-zinc-700 dark:text-zinc-300 font-medium">{decision.decision}</p>
172
+ {decision.rationale && <p className="text-base text-zinc-500 mt-1.5">{decision.rationale}</p>}
157
173
  </div>
158
174
  ))}
159
175
  </div>
160
176
  </div>
161
177
  )}
162
178
 
163
- {/* Children */}
164
179
  {children.length > 0 && (
165
180
  <div className="px-8 py-6">
166
181
  <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-4">
167
182
  {item.type === 'epic' ? 'Features & Chores' : 'Child Items'} ({children.length})
168
183
  </h2>
169
- <WorkItemTree items={children.map(c => ({ ...c, children: [] }))} />
184
+ <WorkItemTree items={children.map(c => ({ ...c, type: c.type, children: [], epic_id: c.parent_id, rejection_round: c.rejection_round ?? null, rejection_history: c.rejection_history ?? null }))} />
170
185
  </div>
171
186
  )}
172
187
  </div>