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,18 +1,17 @@
1
- 'use client';
2
-
3
1
  import { ConnectClaudeScreen } from '@/components/ConnectClaudeScreen';
2
+ import { isTauri, claudeCode } from '@/lib/tauri-bridge';
4
3
 
5
4
  export default function ConnectClaudePage() {
6
5
  const handleConnect = async () => {
7
- if (!window.electronAPI?.isElectron) {
6
+ if (!isTauri()) {
8
7
  return { success: false, error: 'Only available in the desktop app.' };
9
8
  }
10
- return await window.electronAPI.claudeCode.login();
9
+ return await claudeCode.login();
11
10
  };
12
11
 
13
12
  const handleCheckAuth = async () => {
14
- if (!window.electronAPI?.isElectron) return false;
15
- return await window.electronAPI.claudeCode.isAuthenticated();
13
+ if (!isTauri()) return false;
14
+ return await claudeCode.isAuthenticated();
16
15
  };
17
16
 
18
17
  return (
@@ -1,41 +1,65 @@
1
- import { getDecision } from '@/lib/db';
2
- import Link from 'next/link';
3
- import { notFound } from 'next/navigation';
1
+ import { useState, useEffect } from 'react';
2
+ import { useParams, Link } from 'react-router-dom';
3
+ import { dataBridge } from '@/lib/data-bridge';
4
+ import type { DecisionData } from '@/lib/data-bridge';
4
5
 
5
- interface PageProps {
6
- params: Promise<{ id: string }>;
7
- }
6
+ export default function DecisionPage() {
7
+ const { id } = useParams<{ id: string }>();
8
+ const [decision, setDecision] = useState<DecisionData | null>(null);
9
+ const [loading, setLoading] = useState(true);
10
+ const [notFound, setNotFound] = useState(false);
8
11
 
9
- export default async function DecisionPage({ params }: PageProps) {
10
- const { id } = await params;
11
- const decisionId = parseInt(id, 10);
12
+ useEffect(() => {
13
+ async function loadData() {
14
+ const decisionId = parseInt(id || '', 10);
15
+ if (isNaN(decisionId)) {
16
+ setNotFound(true);
17
+ setLoading(false);
18
+ return;
19
+ }
12
20
 
13
- if (isNaN(decisionId)) {
14
- notFound();
15
- }
21
+ try {
22
+ const data = await dataBridge.getDecision(decisionId);
23
+ if (!data) {
24
+ setNotFound(true);
25
+ } else {
26
+ setDecision(data);
27
+ }
28
+ } catch {
29
+ setNotFound(true);
30
+ } finally {
31
+ setLoading(false);
32
+ }
33
+ }
34
+ loadData();
35
+ }, [id]);
16
36
 
17
- const decision = getDecision(decisionId);
37
+ if (loading) return null;
18
38
 
19
- if (!decision) {
20
- notFound();
39
+ if (notFound || !decision) {
40
+ return (
41
+ <div className="flex-1 flex items-center justify-center">
42
+ <div className="text-center">
43
+ <h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-100 mb-2">Not Found</h1>
44
+ <p className="text-zinc-500 mb-4">Decision not found.</p>
45
+ <Link to="/" viewTransition className="text-[#5a7d7f] hover:underline">← Back to Dashboard</Link>
46
+ </div>
47
+ </div>
48
+ );
21
49
  }
22
50
 
23
51
  return (
24
- <div className="min-h-screen bg-zinc-50 dark:bg-zinc-950">
25
- {/* Header */}
52
+ <div className="flex-1 overflow-y-auto bg-zinc-50 dark:bg-zinc-950">
26
53
  <header className="border-b border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900">
27
54
  <div className="max-w-4xl mx-auto px-6 py-6">
28
- <Link href="/" className="text-[#5a7d7f] dark:text-[#a3bfc0] hover:underline text-base">
29
- ← Back to Dashboard
30
- </Link>
55
+ <Link to="/" viewTransition className="text-[#5a7d7f] dark:text-[#a3bfc0] hover:underline text-base">← Back to Dashboard</Link>
31
56
  </div>
32
57
  </header>
33
58
 
34
59
  <main className="max-w-4xl mx-auto px-4 py-6">
35
- {/* Breadcrumb to parent work item */}
36
60
  {decision.work_item_id && (
37
61
  <div className="mb-6 text-base text-zinc-500">
38
- <Link href={`/work/${decision.work_item_id}`} className="hover:underline">
62
+ <Link to={`/work/${decision.work_item_id}`} viewTransition className="hover:underline">
39
63
  #{decision.work_item_id} {decision.work_item_title}
40
64
  </Link>
41
65
  <span className="mx-2">→</span>
@@ -43,9 +67,7 @@ export default async function DecisionPage({ params }: PageProps) {
43
67
  </div>
44
68
  )}
45
69
 
46
- {/* Main card */}
47
70
  <div className="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 overflow-hidden">
48
- {/* Header */}
49
71
  <div className="px-8 py-6 border-b border-zinc-200 dark:border-zinc-800">
50
72
  <div className="flex items-start justify-between gap-6">
51
73
  <div>
@@ -54,54 +76,37 @@ export default async function DecisionPage({ params }: PageProps) {
54
76
  <span>•</span>
55
77
  <span className="font-mono">#{decision.id}</span>
56
78
  </div>
57
- <h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
58
- {decision.aspect}
59
- </h1>
79
+ <h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{decision.aspect}</h1>
60
80
  </div>
61
81
  </div>
62
82
  </div>
63
83
 
64
- {/* Decision content */}
65
84
  <div className="px-8 py-6 border-b border-zinc-200 dark:border-zinc-800">
66
- <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-3">
67
- Decision
68
- </h2>
69
- <p className="text-zinc-700 dark:text-zinc-300 whitespace-pre-wrap text-lg">
70
- {decision.decision}
71
- </p>
85
+ <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-3">Decision</h2>
86
+ <p className="text-zinc-700 dark:text-zinc-300 whitespace-pre-wrap text-lg">{decision.decision}</p>
72
87
  </div>
73
88
 
74
- {/* Rationale */}
75
89
  {decision.rationale && (
76
90
  <div className="px-8 py-6 border-b border-zinc-200 dark:border-zinc-800">
77
- <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-3">
78
- Rationale
79
- </h2>
80
- <p className="text-zinc-700 dark:text-zinc-300 whitespace-pre-wrap">
81
- {decision.rationale}
82
- </p>
91
+ <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-3">Rationale</h2>
92
+ <p className="text-zinc-700 dark:text-zinc-300 whitespace-pre-wrap">{decision.rationale}</p>
83
93
  </div>
84
94
  )}
85
95
 
86
- {/* Metadata */}
87
96
  <div className="px-8 py-6">
88
- <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-4">
89
- Details
90
- </h2>
97
+ <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-4">Details</h2>
91
98
  <dl className="grid grid-cols-2 gap-6 text-base">
92
99
  <div>
93
100
  <dt className="text-zinc-500">Related Work Item</dt>
94
101
  <dd className="text-zinc-900 dark:text-zinc-100">
95
- <Link href={`/work/${decision.work_item_id}`} className="hover:underline text-[#5a7d7f] dark:text-[#a3bfc0]">
102
+ <Link to={`/work/${decision.work_item_id}`} viewTransition className="hover:underline text-[#5a7d7f] dark:text-[#a3bfc0]">
96
103
  #{decision.work_item_id} {decision.work_item_title}
97
104
  </Link>
98
105
  </dd>
99
106
  </div>
100
107
  <div>
101
108
  <dt className="text-zinc-500">Created</dt>
102
- <dd className="text-zinc-900 dark:text-zinc-100">
103
- {new Date(decision.created_at).toLocaleDateString()}
104
- </dd>
109
+ <dd className="text-zinc-900 dark:text-zinc-100">{new Date(decision.created_at).toLocaleDateString()}</dd>
105
110
  </div>
106
111
  </dl>
107
112
  </div>
@@ -1,5 +1,3 @@
1
- 'use client';
2
-
3
1
  import { useState, useEffect, useCallback } from 'react';
4
2
  import { GateCard } from '@/components/GateCard';
5
3
  import { GateChoiceCard } from '@/components/GateChoiceCard';
@@ -124,7 +122,7 @@ export default function GateDemoPage() {
124
122
  const [activeTab, setActiveTab] = useState<DemoTab>('workflow');
125
123
 
126
124
  return (
127
- <div className="min-h-screen bg-zinc-100 dark:bg-zinc-900 p-10">
125
+ <div className="flex-1 overflow-y-auto bg-zinc-100 dark:bg-zinc-900 p-10">
128
126
  <div className="max-w-5xl mx-auto">
129
127
  {/* Header */}
130
128
  <div className="mb-8">
@@ -254,7 +252,7 @@ function WorkflowDemo() {
254
252
  <button
255
253
  onClick={play}
256
254
  disabled={isPlaying}
257
- className="px-5 py-3 bg-zinc-900 dark:bg-zinc-100 hover:bg-zinc-800 dark:hover:bg-zinc-200 disabled:opacity-50 text-white dark:text-zinc-900 text-base font-medium rounded-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0"
255
+ className="px-5 py-3 bg-zinc-900 dark:bg-zinc-100 hover:bg-zinc-800 dark:hover:bg-zinc-200 disabled:opacity-50 text-white dark:text-zinc-900 text-base font-medium rounded-xl transition-[color,background-color,opacity] duration-200"
258
256
  style={{ boxShadow: '0 1px 2px rgba(0,0,0,0.1), 0 2px 4px rgba(0,0,0,0.06)' }}
259
257
  >
260
258
  {isDone ? 'Replay' : isPaused ? 'Waiting for input...' : isPlaying ? 'Playing...' : 'Play Workflow'}
@@ -296,7 +294,7 @@ function WorkflowDemo() {
296
294
  </div>
297
295
  <div className="h-1 bg-zinc-200 dark:bg-zinc-700 rounded-full overflow-hidden">
298
296
  <div
299
- className="h-full bg-zinc-900 dark:bg-zinc-100 rounded-full transition-all duration-500"
297
+ className="h-full bg-zinc-900 dark:bg-zinc-100 rounded-full transition-[width] duration-500"
300
298
  style={{ width: `${(visibleCount / totalSteps) * 100}%` }}
301
299
  />
302
300
  </div>
@@ -72,7 +72,7 @@ export default function DesignSystemPage() {
72
72
  }
73
73
  `}</style>
74
74
 
75
- <div className="min-h-screen bg-background text-foreground">
75
+ <div className="flex-1 overflow-y-auto bg-background text-foreground">
76
76
  {/* Header */}
77
77
  <header className="sticky top-0 z-10 border-b border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-8 py-6 flex items-center justify-between">
78
78
  <div>
@@ -1,3 +1,20 @@
1
+ /* Font declarations */
2
+ @font-face {
3
+ font-family: 'Satoshi';
4
+ src: url('/fonts/Satoshi-Variable.woff2') format('woff2');
5
+ font-style: normal;
6
+ font-display: swap;
7
+ font-weight: 100 900;
8
+ }
9
+
10
+ @font-face {
11
+ font-family: 'Satoshi';
12
+ src: url('/fonts/Satoshi-VariableItalic.woff2') format('woff2');
13
+ font-style: italic;
14
+ font-display: swap;
15
+ font-weight: 100 900;
16
+ }
17
+
1
18
  @import "tailwindcss";
2
19
  @import "tw-animate-css";
3
20
 
@@ -45,6 +62,8 @@
45
62
 
46
63
  :root {
47
64
  --radius: 0.625rem;
65
+ --font-satoshi: 'Satoshi', system-ui, sans-serif;
66
+ --font-geist-mono: 'Geist Mono Variable', 'Geist Mono', monospace;
48
67
  --background: oklch(0.98 0.002 80);
49
68
  --foreground: oklch(0.145 0 0);
50
69
  --card: oklch(1 0 0);
@@ -113,14 +132,67 @@
113
132
  }
114
133
 
115
134
  @layer base {
116
- * {
117
- @apply border-border outline-ring/50;
135
+ *,
136
+ ::before,
137
+ ::after {
138
+ border-color: var(--color-border);
118
139
  }
119
140
  body {
120
141
  @apply bg-background text-foreground;
142
+ font-family: var(--font-satoshi);
143
+ -webkit-font-smoothing: antialiased;
144
+ -moz-osx-font-smoothing: grayscale;
121
145
  }
122
146
  }
123
147
 
124
148
  html {
125
149
  font-size: 16px;
126
150
  }
151
+
152
+ /* Kanban card hover — instant shadow swap, no transition (WebKit-friendly) */
153
+ .kanban-card {
154
+ contain: content;
155
+ content-visibility: auto;
156
+ }
157
+ .kanban-card:hover {
158
+ box-shadow: var(--hover-shadow) !important;
159
+ transform: translateY(-2px);
160
+ }
161
+
162
+ /* Highlight pulse — CSS-only, no framer-motion runtime cost */
163
+ @keyframes highlight-pulse {
164
+ 0%, 100% { outline: 3px solid rgba(129, 157, 159, 0); outline-offset: 0; }
165
+ 50% { outline: 3px solid rgba(129, 157, 159, 0.4); outline-offset: 0; }
166
+ }
167
+
168
+ /* View Transitions API — fast cross-fade for route changes */
169
+ ::view-transition-old(root) {
170
+ animation: vt-fade-out 120ms ease-out;
171
+ }
172
+ ::view-transition-new(root) {
173
+ animation: vt-fade-in 200ms ease-out;
174
+ }
175
+ @keyframes vt-fade-out {
176
+ to { opacity: 0; }
177
+ }
178
+ @keyframes vt-fade-in {
179
+ from { opacity: 0; }
180
+ }
181
+
182
+ /* Skeleton loading pulse */
183
+ @keyframes skeleton-pulse {
184
+ 0%, 100% { opacity: 0.5; }
185
+ 50% { opacity: 0.25; }
186
+ }
187
+
188
+ /* Respect reduced motion globally — disables all animations/transitions */
189
+ @media (prefers-reduced-motion: reduce) {
190
+ *,
191
+ ::before,
192
+ ::after {
193
+ animation-duration: 0.01ms !important;
194
+ animation-iteration-count: 1 !important;
195
+ transition-duration: 0.01ms !important;
196
+ scroll-behavior: auto !important;
197
+ }
198
+ }
@@ -1,7 +1,6 @@
1
- 'use client';
2
-
3
1
  import { useState } from 'react';
4
2
  import { InstallClaudeScreen } from '@/components/InstallClaudeScreen';
3
+ import { isTauri, claudeCode } from '@/lib/tauri-bridge';
5
4
 
6
5
  export default function InstallClaudePage() {
7
6
  const [isInstalling, setIsInstalling] = useState(false);
@@ -12,15 +11,14 @@ export default function InstallClaudePage() {
12
11
  setError(null);
13
12
  setIsInstalling(true);
14
13
 
15
- // Check if we're in Electron
16
- if (!window.electronAPI?.isElectron) {
14
+ if (!isTauri()) {
17
15
  setError('Installation is only available in the desktop app.');
18
16
  setIsInstalling(false);
19
17
  return;
20
18
  }
21
19
 
22
20
  try {
23
- const result = await window.electronAPI.claudeCode.install();
21
+ const result = await claudeCode.install();
24
22
 
25
23
  if (!result.success) {
26
24
  setError(result.error || 'Installation failed');
@@ -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
 
@@ -17,18 +15,18 @@ export default function LoginPage() {
17
15
  const [error, setError] = useState<string | null>(null);
18
16
 
19
17
  // Poll for auth completion after Google sign-in.
20
- // The deep link handler in main.js saves the token — this polling detects it
21
- // and navigates to the dashboard even if mainWindow.loadURL doesn't fire.
18
+ // The deep link handler saves the token — this polling detects it
19
+ // and navigates to the dashboard.
22
20
  const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
23
21
 
24
22
  // Redirect already-authenticated users to dashboard
25
23
  useEffect(() => {
26
24
  async function checkIfAlreadyAuthenticated() {
27
- if (window.electronAPI?.isElectron) {
25
+ if (isTauri()) {
28
26
  try {
29
- const status = await window.electronAPI.auth.getStatus();
27
+ const status = await auth.getStatus();
30
28
  if (status.authenticated) {
31
- const path = await window.electronAPI.auth.getPostLoginPath?.() || '/';
29
+ const path = await auth.getPostLoginPath() || '/';
32
30
  window.location.href = path;
33
31
  }
34
32
  } catch {
@@ -46,16 +44,16 @@ export default function LoginPage() {
46
44
  }, []);
47
45
 
48
46
  const handleGoogleSignIn = () => {
49
- if (!window.electronAPI?.isElectron) return;
50
- window.electronAPI.auth.loginWithGoogle();
47
+ if (!isTauri()) return;
48
+ auth.loginWithGoogle();
51
49
 
52
50
  // Start polling for auth status (token saved by deep link handler)
53
51
  pollRef.current = setInterval(async () => {
54
52
  try {
55
- const status = await window.electronAPI!.auth.getStatus();
53
+ const status = await auth.getStatus();
56
54
  if (status.authenticated) {
57
55
  if (pollRef.current) clearInterval(pollRef.current);
58
- const path = await window.electronAPI!.auth.getPostLoginPath?.() || '/';
56
+ const path = await auth.getPostLoginPath() || '/';
59
57
  window.location.href = path;
60
58
  }
61
59
  } catch {
@@ -123,12 +121,12 @@ export default function LoginPage() {
123
121
 
124
122
  const data = await res.json() as { token: string; user: { id: string; email: string; plan: string } };
125
123
 
126
- // Save auth state via Electron IPC
127
- if (window.electronAPI?.isElectron) {
128
- await window.electronAPI.auth.saveToken(data.token, data.user);
124
+ // Save auth state via Tauri IPC
125
+ if (isTauri()) {
126
+ await auth.saveToken(data.token, data.user);
129
127
  }
130
128
 
131
- const path = await window.electronAPI?.auth.getPostLoginPath?.() || '/';
129
+ const path = await auth.getPostLoginPath() || '/';
132
130
  window.location.href = path;
133
131
  } catch {
134
132
  setError('Failed to verify code. Check your connection.');
@@ -141,12 +139,11 @@ export default function LoginPage() {
141
139
  <div className="max-w-md w-full space-y-10">
142
140
  {/* Logo */}
143
141
  <div className="flex flex-col items-center space-y-6">
144
- <Image
142
+ <img
145
143
  src="/jettypod_wordmark.png"
146
144
  alt="JettyPod"
147
145
  width={160}
148
146
  height={40}
149
- priority
150
147
  />
151
148
  <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
152
149
  Sign in to JettyPod
@@ -240,7 +237,7 @@ export default function LoginPage() {
240
237
  {/* Switch to signup */}
241
238
  <p className="text-center text-base text-zinc-500 dark:text-zinc-400">
242
239
  Don&apos;t have an account?{' '}
243
- <Link href="/signup" className="font-medium hover:underline" style={{ color: '#819D9F' }}>
240
+ <Link to="/signup" className="font-medium hover:underline" style={{ color: '#819D9F' }}>
244
241
  Create one
245
242
  </Link>
246
243
  </p>
@@ -1,61 +1,114 @@
1
- import { redirect } from 'next/navigation';
2
- import { getKanbanData, hasProject, isBlankProject, hasOnboardingStarted, getProjectRoot } from '@/lib/db';
1
+ import { useState, useEffect } from 'react';
2
+ import { useNavigate, useLocation, Outlet } from 'react-router-dom';
3
3
  import { RealTimeKanbanWrapper } from '@/components/RealTimeKanbanWrapper';
4
-
5
- // Force dynamic rendering - database is only available at runtime
6
- export const dynamic = 'force-dynamic';
4
+ import { dataBridge, prefetch } from '@/lib/data-bridge';
5
+ import type { KanbanData } from '@/lib/data-bridge';
7
6
 
8
7
  export default function Home() {
9
- // Check if a project is selected - if not, redirect to welcome
10
- if (!hasProject()) {
11
- redirect('/welcome');
12
- }
8
+ const navigate = useNavigate();
9
+ const { pathname } = useLocation();
10
+ const isChildRoute = pathname !== '/';
11
+ const [data, setData] = useState<KanbanData | null>(null);
12
+ const [projectPath, setProjectPath] = useState('');
13
+ const [isBlank, setIsBlank] = useState(false);
14
+ const [error, setError] = useState<string | null>(null);
15
+ const [loading, setLoading] = useState(true);
13
16
 
14
- try {
15
- const data = getKanbanData();
16
- const projectRoot = getProjectRoot();
17
- const isBlank = projectRoot ? isBlankProject(projectRoot) && !hasOnboardingStarted() : false;
17
+ useEffect(() => {
18
+ async function loadData() {
19
+ try {
20
+ // getProjectRoot() is cached after first call no redundant IPC
21
+ const root = await dataBridge.getProjectRoot();
22
+ if (!root) {
23
+ navigate('/welcome', { replace: true });
24
+ return;
25
+ }
18
26
 
19
- // Serialize Map data for client component
20
- const serializedData = {
21
- inFlight: data.inFlight,
22
- backlog: Array.from(data.backlog.entries()),
23
- done: Array.from(data.done.entries()),
24
- };
27
+ const kanbanData = await prefetch.backlog();
25
28
 
26
- return (
27
- <div className="h-full flex flex-col min-h-0 overflow-hidden max-w-7xl w-full mx-auto px-4 py-4">
28
- <RealTimeKanbanWrapper initialData={serializedData} isBlank={isBlank} projectPath={projectRoot || ''} />
29
- </div>
30
- );
31
- } catch (error) {
32
- const errorMessage = error instanceof Error ? error.message : String(error);
33
- const errorStack = error instanceof Error ? error.stack : undefined;
29
+ setData(kanbanData);
30
+ setProjectPath(root || '');
31
+ // isBlank detection: fresh project with only the seeded "Project Planning" epic
32
+ const onlyGroup = kanbanData.backlog.size === 1
33
+ ? [...kanbanData.backlog.values()][0]
34
+ : null;
35
+ const hasOnlyOnboarding = kanbanData.inFlight.length === 0
36
+ && kanbanData.done.size === 0
37
+ && onlyGroup?.epicTitle === 'Project Planning';
38
+ setIsBlank(hasOnlyOnboarding);
39
+ } catch (err) {
40
+ setError(err instanceof Error ? err.message : String(err));
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ }
45
+ loadData();
46
+ }, [navigate]);
34
47
 
35
- // Log to server console (visible in Electron logs)
36
- console.error('[Home Page Error]', errorMessage);
37
- console.error('[Home Page Stack]', errorStack);
38
- console.error('[JETTYPOD_PROJECT_PATH]', process.env.JETTYPOD_PROJECT_PATH);
48
+ // Serialize Map data for RealTimeKanbanWrapper (it expects this format)
49
+ const serializedData = data ? {
50
+ inFlight: data.inFlight,
51
+ backlog: Array.from(data.backlog.entries()),
52
+ done: Array.from(data.done.entries()),
53
+ } : null;
39
54
 
40
- // Return error UI so user can see what's wrong
41
- return (
42
- <div className="flex-1 flex items-center justify-center p-8">
43
- <div className="max-w-2xl w-full bg-red-50 border-2 border-red-200 rounded-lg p-8">
44
- <h1 className="text-xl font-bold text-red-800 mb-4">Failed to load project</h1>
45
- <div className="bg-white border-2 border-red-100 rounded p-6 mb-6">
46
- <p className="font-mono text-base text-red-700 whitespace-pre-wrap">{errorMessage}</p>
55
+ // Kanban board content based on loading/error/data state
56
+ const kanbanContent = loading ? (
57
+ <div className="max-w-7xl w-full mx-auto px-4 py-4">
58
+ <div className="flex gap-4" style={{ height: 'calc(var(--main-h, 100vh) - 2rem)' }}>
59
+ {/* Backlog column skeleton */}
60
+ <div className="flex-1 max-w-[600px] flex flex-col min-h-0">
61
+ <div className="bg-zinc-100 dark:bg-zinc-900 rounded-xl p-4 flex-1 min-h-0">
62
+ <div className="flex items-center justify-between mb-4">
63
+ <div className="h-6 w-24 bg-zinc-200 dark:bg-zinc-800 rounded" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
64
+ <div className="h-6 w-10 bg-zinc-200 dark:bg-zinc-800 rounded-full" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
65
+ </div>
66
+ <div className="space-y-3">
67
+ {[1, 2, 3, 4, 5].map(i => (
68
+ <div key={i} className="h-20 bg-zinc-200 dark:bg-zinc-800 rounded-xl" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
69
+ ))}
70
+ </div>
47
71
  </div>
48
- <div className="text-base text-gray-600 space-y-1.5">
49
- <p><strong>Project path:</strong> {process.env.JETTYPOD_PROJECT_PATH || '(not set)'}</p>
72
+ </div>
73
+ {/* Done column skeleton */}
74
+ <div className="flex-1 max-w-[600px] flex flex-col min-h-0">
75
+ <div className="bg-zinc-100 dark:bg-zinc-900 rounded-xl p-4 flex-1 min-h-0">
76
+ <div className="flex items-center justify-between mb-4">
77
+ <div className="h-6 w-16 bg-zinc-200 dark:bg-zinc-800 rounded" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
78
+ <div className="h-6 w-10 bg-zinc-200 dark:bg-zinc-800 rounded-full" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
79
+ </div>
80
+ <div className="space-y-3">
81
+ {[1, 2, 3].map(i => (
82
+ <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' }} />
83
+ ))}
84
+ </div>
50
85
  </div>
51
- {errorStack && (
52
- <details className="mt-4">
53
- <summary className="text-base text-gray-500 cursor-pointer">Stack trace</summary>
54
- <pre className="mt-2 text-base text-gray-500 overflow-auto p-3 bg-gray-50 rounded">{errorStack}</pre>
55
- </details>
56
- )}
57
86
  </div>
58
87
  </div>
59
- );
60
- }
88
+ </div>
89
+ ) : error ? (
90
+ <div className="flex-1 flex items-center justify-center p-8">
91
+ <div className="max-w-2xl w-full bg-red-50 border-2 border-red-200 rounded-lg p-8">
92
+ <h1 className="text-xl font-bold text-red-800 mb-4">Failed to load project</h1>
93
+ <div className="bg-white border-2 border-red-100 rounded p-6 mb-6">
94
+ <p className="font-mono text-base text-red-700 whitespace-pre-wrap">{error}</p>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ ) : serializedData ? (
99
+ <div className="max-w-7xl w-full mx-auto px-4 py-4">
100
+ <RealTimeKanbanWrapper initialData={serializedData} isBlank={isBlank} projectPath={projectPath} />
101
+ </div>
102
+ ) : null;
103
+
104
+ return (
105
+ <>
106
+ {/* Kanban board — always mounted, hidden when viewing a child route */}
107
+ <div style={isChildRoute ? { display: 'none' } : undefined}>
108
+ {kanbanContent}
109
+ </div>
110
+ {/* Child route content (work detail, proof dashboard, decision) */}
111
+ <Outlet />
112
+ </>
113
+ );
61
114
  }