jettypod 4.4.118 → 4.4.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/.env +4 -3
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
  8. package/apps/dashboard/app/demo/gates/page.tsx +43 -45
  9. package/apps/dashboard/app/design-system/page.tsx +868 -0
  10. package/apps/dashboard/app/globals.css +80 -4
  11. package/apps/dashboard/app/install-claude/page.tsx +4 -6
  12. package/apps/dashboard/app/login/page.tsx +72 -54
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +61 -13
  15. package/apps/dashboard/app/signup/page.tsx +242 -0
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +13 -16
  19. package/apps/dashboard/app/work/[id]/page.tsx +117 -118
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +92 -85
  22. package/apps/dashboard/components/CardMenu.tsx +45 -12
  23. package/apps/dashboard/components/ClaudePanel.tsx +771 -850
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
  26. package/apps/dashboard/components/CopyableId.tsx +3 -4
  27. package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
  28. package/apps/dashboard/components/DragContext.tsx +134 -63
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +6 -7
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +26 -7
  34. package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
  35. package/apps/dashboard/components/EpicGroup.tsx +359 -0
  36. package/apps/dashboard/components/GateCard.tsx +79 -17
  37. package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
  39. package/apps/dashboard/components/JettyLoader.tsx +37 -0
  40. package/apps/dashboard/components/KanbanBoard.tsx +368 -958
  41. package/apps/dashboard/components/KanbanCard.tsx +740 -0
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
  44. package/apps/dashboard/components/MainNav.tsx +38 -73
  45. package/apps/dashboard/components/MessageBlock.tsx +468 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -16
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
  53. package/apps/dashboard/components/ReviewFooter.tsx +139 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -19
  55. package/apps/dashboard/components/SubscribeContent.tsx +91 -47
  56. package/apps/dashboard/components/TestTree.tsx +16 -16
  57. package/apps/dashboard/components/TipCard.tsx +16 -17
  58. package/apps/dashboard/components/Toast.tsx +5 -6
  59. package/apps/dashboard/components/TypeIcon.tsx +55 -0
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
  62. package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
  64. package/apps/dashboard/components/WorkItemTree.tsx +11 -32
  65. package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
  72. package/apps/dashboard/components/ui/Button.tsx +104 -0
  73. package/apps/dashboard/components/ui/Input.tsx +78 -0
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
  77. package/apps/dashboard/contexts/UsageContext.tsx +87 -32
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  83. package/apps/dashboard/index.html +73 -0
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/data-bridge.ts +722 -0
  86. package/apps/dashboard/lib/db.ts +69 -1265
  87. package/apps/dashboard/lib/environment-config.ts +173 -0
  88. package/apps/dashboard/lib/environment-verification.ts +119 -0
  89. package/apps/dashboard/lib/kanban-utils.ts +270 -0
  90. package/apps/dashboard/lib/proof-run.ts +495 -0
  91. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  92. package/apps/dashboard/lib/run-migrations.js +27 -2
  93. package/apps/dashboard/lib/service-recovery.ts +326 -0
  94. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  95. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  96. package/apps/dashboard/lib/session-stream-manager.ts +308 -134
  97. package/apps/dashboard/lib/shadows.ts +7 -0
  98. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  99. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  100. package/apps/dashboard/lib/tauri.ts +106 -0
  101. package/apps/dashboard/lib/utils.ts +6 -0
  102. package/apps/dashboard/next-env.d.ts +1 -1
  103. package/apps/dashboard/package.json +21 -32
  104. package/apps/dashboard/public/bug-icon.png +0 -0
  105. package/apps/dashboard/public/buoy-icon.png +0 -0
  106. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  107. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  108. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  109. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  110. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  111. package/apps/dashboard/public/jettypod_logo.png +0 -0
  112. package/apps/dashboard/public/pier-icon.png +0 -0
  113. package/apps/dashboard/public/star-icon.png +0 -0
  114. package/apps/dashboard/public/wrench-icon.png +0 -0
  115. package/apps/dashboard/scripts/tauri-build.js +228 -0
  116. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  117. package/apps/dashboard/scripts/ws-server.js +191 -0
  118. package/apps/dashboard/src/main.tsx +12 -0
  119. package/apps/dashboard/src/router.tsx +107 -0
  120. package/apps/dashboard/src/vite-env.d.ts +1 -0
  121. package/apps/dashboard/tsconfig.json +7 -12
  122. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  123. package/apps/dashboard/vite.config.ts +33 -0
  124. package/apps/update-server/src/index.ts +228 -80
  125. package/claude-hooks/global-guardrails.js +14 -13
  126. package/crates/jettypod-cli/Cargo.toml +19 -0
  127. package/crates/jettypod-cli/src/commands.rs +1249 -0
  128. package/crates/jettypod-cli/src/main.rs +595 -0
  129. package/crates/jettypod-core/Cargo.toml +26 -0
  130. package/crates/jettypod-core/build.rs +98 -0
  131. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  132. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  133. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  134. package/crates/jettypod-core/src/auth.rs +294 -0
  135. package/crates/jettypod-core/src/config.rs +397 -0
  136. package/crates/jettypod-core/src/db/mod.rs +507 -0
  137. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  138. package/crates/jettypod-core/src/db/startup.rs +101 -0
  139. package/crates/jettypod-core/src/db/validate.rs +149 -0
  140. package/crates/jettypod-core/src/error.rs +76 -0
  141. package/crates/jettypod-core/src/git.rs +458 -0
  142. package/crates/jettypod-core/src/lib.rs +20 -0
  143. package/crates/jettypod-core/src/sessions.rs +625 -0
  144. package/crates/jettypod-core/src/skills.rs +556 -0
  145. package/crates/jettypod-core/src/work.rs +1086 -0
  146. package/crates/jettypod-core/src/worktree.rs +628 -0
  147. package/crates/jettypod-core/src/ws.rs +767 -0
  148. package/cucumber-test.cjs +6 -0
  149. package/cucumber.js +9 -3
  150. package/docs/COMMAND_REFERENCE.md +34 -0
  151. package/hooks/post-checkout +32 -75
  152. package/hooks/post-merge +111 -10
  153. package/jest.setup.js +1 -0
  154. package/jettypod.js +145 -116
  155. package/lib/bdd-preflight.js +96 -0
  156. package/lib/chore-taxonomy.js +33 -10
  157. package/lib/database.js +36 -16
  158. package/lib/db-watcher.js +1 -1
  159. package/lib/git-hooks/pre-commit +1 -1
  160. package/lib/jettypod-backup.js +27 -4
  161. package/lib/merge-lock.js +111 -253
  162. package/lib/migrations/027-plan-at-creation-column.js +3 -1
  163. package/lib/migrations/029-remove-autoincrement.js +307 -0
  164. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  165. package/lib/migrations/030-rejection-round-columns.js +54 -0
  166. package/lib/migrations/031-session-isolation-index.js +17 -0
  167. package/lib/migrations/index.js +47 -4
  168. package/lib/schema.js +10 -5
  169. package/lib/seed-onboarding.js +1 -1
  170. package/lib/update-command/index.js +9 -175
  171. package/lib/work-commands/index.js +144 -19
  172. package/lib/work-tracking/index.js +148 -27
  173. package/lib/worktree-diagnostics.js +16 -16
  174. package/lib/worktree-facade.js +1 -1
  175. package/lib/worktree-manager.js +8 -8
  176. package/lib/worktree-reconciler.js +5 -5
  177. package/package.json +9 -2
  178. package/scripts/ndjson-to-cucumber-json.js +152 -0
  179. package/scripts/postinstall.js +25 -0
  180. package/skills-templates/bug-mode/SKILL.md +79 -20
  181. package/skills-templates/bug-planning/SKILL.md +25 -29
  182. package/skills-templates/chore-mode/SKILL.md +171 -69
  183. package/skills-templates/chore-mode/verification.js +51 -10
  184. package/skills-templates/chore-planning/SKILL.md +47 -18
  185. package/skills-templates/design-system-selection/SKILL.md +273 -0
  186. package/skills-templates/epic-planning/SKILL.md +82 -48
  187. package/skills-templates/external-transition/SKILL.md +47 -47
  188. package/skills-templates/feature-planning/SKILL.md +173 -74
  189. package/skills-templates/production-mode/SKILL.md +69 -49
  190. package/skills-templates/request-routing/SKILL.md +4 -4
  191. package/skills-templates/simple-improvement/SKILL.md +74 -29
  192. package/skills-templates/speed-mode/SKILL.md +217 -141
  193. package/skills-templates/stable-mode/SKILL.md +148 -89
  194. package/apps/dashboard/README.md +0 -36
  195. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
  196. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  197. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
  198. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  199. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
  200. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  201. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  202. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  203. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  204. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  205. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  206. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  207. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  208. package/apps/dashboard/app/api/tests/route.ts +0 -9
  209. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  210. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  211. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  212. package/apps/dashboard/app/api/usage/route.ts +0 -17
  213. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  214. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  215. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  216. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
  217. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  218. package/apps/dashboard/app/layout.tsx +0 -43
  219. package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
  220. package/apps/dashboard/electron/ipc-handlers.js +0 -1028
  221. package/apps/dashboard/electron/main.js +0 -2124
  222. package/apps/dashboard/electron/preload.js +0 -123
  223. package/apps/dashboard/electron/session-manager.js +0 -141
  224. package/apps/dashboard/electron-builder.config.js +0 -357
  225. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  226. package/apps/dashboard/lib/claude-process-manager.ts +0 -492
  227. package/apps/dashboard/lib/db-bridge.ts +0 -282
  228. package/apps/dashboard/lib/prototypes.ts +0 -202
  229. package/apps/dashboard/lib/test-results-db.ts +0 -307
  230. package/apps/dashboard/lib/tests.ts +0 -282
  231. package/apps/dashboard/next.config.js +0 -50
  232. package/apps/dashboard/postcss.config.mjs +0 -7
  233. package/apps/dashboard/public/file.svg +0 -1
  234. package/apps/dashboard/public/globe.svg +0 -1
  235. package/apps/dashboard/public/next.svg +0 -1
  236. package/apps/dashboard/public/vercel.svg +0 -1
  237. package/apps/dashboard/public/window.svg +0 -1
  238. package/apps/dashboard/scripts/download-node.js +0 -104
  239. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
  240. package/docs/bdd-guidance.md +0 -390
@@ -1,8 +1,8 @@
1
- 'use client';
2
-
3
1
  import { useState, useEffect } from 'react';
4
- import { motion, AnimatePresence, useReducedMotion } from 'framer-motion';
5
- import ReactMarkdown from 'react-markdown';
2
+ import { m, AnimatePresence, useReducedMotion } from 'framer-motion';
3
+ import { lazy, Suspense } from 'react';
4
+
5
+ const LazyMarkdown = lazy(() => import('./LazyMarkdown'));
6
6
 
7
7
  const STORAGE_KEY = 'jettypod-dismissed-tips';
8
8
 
@@ -27,8 +27,7 @@ function dismissTip(tipId: string): void {
27
27
  }
28
28
  }
29
29
 
30
- // Multi-layer shadow matching kanban card system
31
- const CARD_SHADOW = '0 1px 2px rgba(0,0,0,0.02)';
30
+ import { shadow } from '@/lib/shadows';
32
31
 
33
32
  interface TipCardProps {
34
33
  tipId: string;
@@ -56,7 +55,7 @@ export function TipCard({ tipId, icon, title, body, onDismiss }: TipCardProps) {
56
55
  return (
57
56
  <AnimatePresence>
58
57
  {!dismissed && (
59
- <motion.div
58
+ <m.div
60
59
  data-testid={`tip-card-${tipId}`}
61
60
  initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 8 }}
62
61
  animate={prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}
@@ -67,14 +66,14 @@ export function TipCard({ tipId, icon, title, body, onDismiss }: TipCardProps) {
67
66
  transition={{ duration: prefersReducedMotion ? 0.15 : 0.35, ease: [0.22, 1, 0.36, 1] }}
68
67
  style={{
69
68
  background: 'linear-gradient(135deg, #f0fdfa 0%, #f0fdfb 100%)',
70
- border: '1px solid #ccfbf1',
69
+ border: '2px solid #ccfbf1',
71
70
  borderRadius: 12,
72
71
  padding: 14,
73
- boxShadow: CARD_SHADOW,
72
+ boxShadow: shadow.sm,
74
73
  }}
75
74
  >
76
75
  {/* Header: icon + label/title */}
77
- <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
76
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 10 }}>
78
77
  <div
79
78
  style={{
80
79
  width: 34,
@@ -122,34 +121,34 @@ export function TipCard({ tipId, icon, title, body, onDismiss }: TipCardProps) {
122
121
  fontSize: 13,
123
122
  lineHeight: 1.55,
124
123
  color: '#52525b',
125
- marginLeft: 44,
124
+ marginLeft: 50,
126
125
  }}
127
126
  >
128
127
  {typeof body === 'string' ? (
129
- <ReactMarkdown
128
+ <LazyMarkdown
130
129
  components={{
131
130
  p: ({ children }) => <p style={{ margin: '0 0 8px 0' }}>{children}</p>,
132
131
  strong: ({ children }) => <strong style={{ fontWeight: 600, color: '#3f3f46' }}>{children}</strong>,
133
132
  }}
134
133
  >
135
134
  {body.replace(/\n/g, ' \n')}
136
- </ReactMarkdown>
135
+ </LazyMarkdown>
137
136
  ) : body}
138
137
  </div>
139
138
 
140
139
  {/* Footer: Got it button */}
141
- <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 10 }}>
140
+ <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 12 }}>
142
141
  <button
143
142
  onClick={handleDismiss}
144
143
  data-testid={`tip-dismiss-${tipId}`}
145
144
  style={{
146
145
  background: 'transparent',
147
- border: '1px solid #99f6e4',
146
+ border: '2px solid #99f6e4',
148
147
  fontSize: 12,
149
148
  fontWeight: 600,
150
149
  color: '#0d9488',
151
150
  cursor: 'pointer',
152
- padding: '5px 14px',
151
+ padding: '6px 16px',
153
152
  borderRadius: 8,
154
153
  transition: 'background 0.15s, border-color 0.15s',
155
154
  }}
@@ -165,7 +164,7 @@ export function TipCard({ tipId, icon, title, body, onDismiss }: TipCardProps) {
165
164
  Got it
166
165
  </button>
167
166
  </div>
168
- </motion.div>
167
+ </m.div>
169
168
  )}
170
169
  </AnimatePresence>
171
170
  );
@@ -1,7 +1,6 @@
1
- 'use client';
2
1
 
3
2
  import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
4
- import { AnimatePresence, motion } from 'framer-motion';
3
+ import { AnimatePresence, m } from 'framer-motion';
5
4
 
6
5
  interface Toast {
7
6
  id: string;
@@ -52,16 +51,16 @@ export function ToastProvider({ children }: { children: ReactNode }) {
52
51
 
53
52
  function ToastContainer({ toasts, onDismiss }: { toasts: Toast[]; onDismiss: (id: string) => void }) {
54
53
  return (
55
- <div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2" data-testid="toast-container">
54
+ <div className="fixed bottom-4 right-4 z-50 flex flex-col gap-3" data-testid="toast-container">
56
55
  <AnimatePresence mode="popLayout">
57
56
  {toasts.map(toast => (
58
- <motion.div
57
+ <m.div
59
58
  key={toast.id}
60
59
  initial={{ opacity: 0, y: 20, scale: 0.95 }}
61
60
  animate={{ opacity: 1, y: 0, scale: 1 }}
62
61
  exit={{ opacity: 0, y: -10, scale: 0.95 }}
63
62
  transition={{ duration: 0.2 }}
64
- className={`px-4 py-2 rounded-lg shadow-lg text-sm font-medium cursor-pointer ${
63
+ className={`px-5 py-3 rounded-lg shadow-lg text-base font-medium cursor-pointer ${
65
64
  toast.type === 'success'
66
65
  ? 'bg-green-600 text-white'
67
66
  : toast.type === 'error'
@@ -72,7 +71,7 @@ function ToastContainer({ toasts, onDismiss }: { toasts: Toast[]; onDismiss: (id
72
71
  data-testid="toast"
73
72
  >
74
73
  {toast.message}
75
- </motion.div>
74
+ </m.div>
76
75
  ))}
77
76
  </AnimatePresence>
78
77
  </div>
@@ -0,0 +1,55 @@
1
+
2
+ import { useState, useRef, useCallback, useEffect } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { TYPE_ICONS } from '@/lib/constants';
5
+ import { shadow } from '@/lib/shadows';
6
+
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' },
12
+ };
13
+
14
+ export function TypeIcon({ type, className }: { type: string; className?: string }) {
15
+ const svg = TYPE_ICON_SRCS[type];
16
+ const [tooltip, setTooltip] = useState<{ x: number; y: number } | null>(null);
17
+ const [mounted, setMounted] = useState(false);
18
+ const ref = useRef<HTMLSpanElement>(null);
19
+
20
+ const [portalRoot, setPortalRoot] = useState<HTMLElement | null>(null);
21
+
22
+ useEffect(() => {
23
+ setMounted(true);
24
+ setPortalRoot(document.getElementById('tooltip-root'));
25
+ }, []);
26
+
27
+ const showTooltip = useCallback(() => {
28
+ if (ref.current) {
29
+ const rect = ref.current.getBoundingClientRect();
30
+ setTooltip({ x: rect.left + rect.width / 2, y: rect.bottom + 4 });
31
+ }
32
+ }, []);
33
+
34
+ const hideTooltip = useCallback(() => setTooltip(null), []);
35
+
36
+ if (svg) {
37
+ return (
38
+ <span ref={ref} className="inline-flex" onMouseEnter={showTooltip} onMouseLeave={hideTooltip}>
39
+ <img src={svg.src} alt="" className={`object-contain ${className || "w-6 h-6"}`} />
40
+ {mounted && tooltip && portalRoot && createPortal(
41
+ <span
42
+ className="pointer-events-none fixed"
43
+ style={{ left: tooltip.x, top: tooltip.y, transform: 'translateX(-50%)' }}
44
+ >
45
+ <span className="block rounded-lg bg-white dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 text-xs px-2 py-1 whitespace-nowrap" style={{ boxShadow: shadow.sm }}>
46
+ {svg.label}
47
+ </span>
48
+ </span>,
49
+ portalRoot
50
+ )}
51
+ </span>
52
+ );
53
+ }
54
+ return <>{TYPE_ICONS[type] || '📄'}</>;
55
+ }
@@ -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,8 +1,7 @@
1
- 'use client';
2
1
 
3
2
  import { useState, useEffect, useRef, ReactNode } from 'react';
4
3
 
5
- type AnimationPhase = 'idle' | 'content-fade' | 'video-playing' | 'card-fade' | 'complete';
4
+ type AnimationPhase = 'idle' | 'video-playing' | 'collapsing' | 'complete';
6
5
 
7
6
  interface WaveCompletionAnimationProps {
8
7
  isPlaying: boolean;
@@ -13,16 +12,15 @@ interface WaveCompletionAnimationProps {
13
12
  export function WaveCompletionAnimation({ isPlaying, onComplete, children }: WaveCompletionAnimationProps) {
14
13
  const [phase, setPhase] = useState<AnimationPhase>('idle');
15
14
  const videoRef = useRef<HTMLVideoElement>(null);
15
+ const containerRef = useRef<HTMLDivElement>(null);
16
16
  const hasStartedRef = useRef(false);
17
17
  const timeoutRefs = useRef<NodeJS.Timeout[]>([]);
18
18
 
19
- // Clear all pending timeouts
20
19
  const clearAllTimeouts = () => {
21
20
  timeoutRefs.current.forEach(clearTimeout);
22
21
  timeoutRefs.current = [];
23
22
  };
24
23
 
25
- // Handle video load errors - skip animation and complete immediately
26
24
  const handleVideoError = () => {
27
25
  if (hasStartedRef.current) {
28
26
  clearAllTimeouts();
@@ -35,56 +33,39 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
35
33
  if (isPlaying && !hasStartedRef.current) {
36
34
  hasStartedRef.current = true;
37
35
 
38
- // Check for reduced motion preference
39
- const prefersReducedMotion = typeof window !== 'undefined' &&
40
- window.matchMedia('(prefers-reduced-motion: reduce)').matches;
36
+ const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
41
37
 
42
38
  if (prefersReducedMotion) {
43
- // Skip animation entirely - complete immediately
44
39
  setPhase('complete');
45
40
  onComplete();
46
41
  return;
47
42
  }
48
43
 
49
- // Phase 1: Content fades out (0.5s)
50
- setPhase('content-fade');
44
+ // Phase 1: Content disappears instantly, video starts playing
45
+ setPhase('video-playing');
51
46
 
52
- // Phase 2: Video starts playing
53
47
  const video = videoRef.current;
54
48
  if (video) {
55
49
  video.currentTime = 0;
56
- video.play().catch(() => {
57
- // Video play failed - skip animation
58
- handleVideoError();
59
- });
50
+ video.play().catch(() => handleVideoError());
60
51
  }
61
52
 
53
+ // Phase 2: After 1.5s of video, collapse the card (opacity + height)
62
54
  const t1 = setTimeout(() => {
63
- setPhase('video-playing');
64
- }, 500);
55
+ setPhase('collapsing');
56
+ }, 1500);
65
57
  timeoutRefs.current.push(t1);
66
58
 
67
- // Phase 3: After 5 seconds, card fades out (video keeps playing)
59
+ // Phase 3: After collapse transition (0.5s), fire onComplete
68
60
  const t2 = setTimeout(() => {
69
- setPhase('card-fade');
70
- }, 5000);
71
- timeoutRefs.current.push(t2);
72
-
73
- // Phase 4: After fade completes (1.5s), call onComplete
74
- const t3 = setTimeout(() => {
75
- if (video) {
76
- video.pause();
77
- }
61
+ if (video) video.pause();
78
62
  setPhase('complete');
79
63
  onComplete();
80
- }, 6500); // 5000 + 1500
81
- timeoutRefs.current.push(t3);
64
+ }, 2000);
65
+ timeoutRefs.current.push(t2);
82
66
  }
83
67
  }, [isPlaying, onComplete]);
84
68
 
85
- // Reset when isPlaying becomes false — but only if not already complete.
86
- // If the animation finished naturally (phase === 'complete'), skip the reset
87
- // to avoid snapping opacity back to 1 before the CSS transition visually ends.
88
69
  useEffect(() => {
89
70
  if (!isPlaying && phase !== 'complete') {
90
71
  clearAllTimeouts();
@@ -93,49 +74,55 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
93
74
  }
94
75
  }, [isPlaying, phase]);
95
76
 
96
- // Cleanup on unmount
97
77
  useEffect(() => {
98
78
  return () => clearAllTimeouts();
99
79
  }, []);
100
80
 
101
- const contentOpacity = phase === 'idle' ? 1 : 0;
102
- const videoOpacity = phase === 'idle' ? 0 : 1;
103
- const cardOpacity = phase === 'card-fade' || phase === 'complete' ? 0 : 1;
81
+ const isIdle = phase === 'idle';
82
+ const isCollapsing = phase === 'collapsing' || phase === 'complete';
104
83
 
105
84
  return (
106
85
  <div
107
- className="relative overflow-hidden rounded-xl border"
86
+ ref={containerRef}
108
87
  style={{
109
- opacity: cardOpacity,
110
- transition: 'opacity 1.5s ease-out',
88
+ overflow: 'hidden',
89
+ opacity: isCollapsing ? 0 : 1,
90
+ transform: isCollapsing ? 'scaleY(0)' : 'scaleY(1)',
91
+ transformOrigin: 'top',
92
+ transition: isCollapsing
93
+ ? 'opacity 0.4s ease-out, transform 0.5s ease-out'
94
+ : undefined,
111
95
  }}
112
96
  >
113
- {/* Wave video - positioned behind content */}
114
- <video
115
- ref={videoRef}
116
- className="absolute inset-0 w-full h-full object-cover rounded-xl"
117
- style={{
118
- opacity: videoOpacity,
119
- transition: 'opacity 0.5s ease',
120
- zIndex: 1,
121
- }}
122
- muted
123
- playsInline
124
- src="/assets/wave-completion.mp4"
125
- onError={handleVideoError}
126
- />
127
-
128
- {/* Card content - positioned above video */}
129
- <div
130
- style={{
131
- opacity: contentOpacity,
132
- transition: 'opacity 0.5s ease',
133
- position: 'relative',
134
- zIndex: 2,
135
- background: 'inherit',
136
- }}
137
- >
138
- {children}
97
+ <div className="relative overflow-hidden rounded-xl">
98
+ {/* Wave video - positioned behind content */}
99
+ <video
100
+ ref={videoRef}
101
+ className="absolute inset-0 w-full h-full object-cover rounded-xl"
102
+ style={{
103
+ opacity: isIdle ? 0 : 1,
104
+ transition: 'opacity 0.5s ease-in',
105
+ zIndex: 1,
106
+ }}
107
+ muted
108
+ playsInline
109
+ preload="none"
110
+ src="/assets/wave-completion.mp4"
111
+ onError={handleVideoError}
112
+ />
113
+
114
+ {/* Card content - positioned above video */}
115
+ <div
116
+ style={{
117
+ opacity: isIdle ? 1 : 0,
118
+ transition: 'opacity 0.5s ease-out',
119
+ position: 'relative',
120
+ zIndex: 2,
121
+ background: 'inherit',
122
+ }}
123
+ >
124
+ {children}
125
+ </div>
139
126
  </div>
140
127
  </div>
141
128
  );
@@ -1,7 +1,5 @@
1
- 'use client';
2
-
3
- import Image from 'next/image';
4
- import type { RecentProject } from '@/lib/db-bridge';
1
+ import { Button } from '@/components/ui/Button';
2
+ import type { RecentProject } from '@/lib/tauri-bridge';
5
3
 
6
4
  interface WelcomeScreenProps {
7
5
  recentProjects?: RecentProject[];
@@ -17,16 +15,15 @@ export function WelcomeScreen({
17
15
  onSelectRecentProject,
18
16
  }: WelcomeScreenProps) {
19
17
  return (
20
- <div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900 p-8">
21
- <div className="max-w-md w-full space-y-8">
18
+ <div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900 px-8 py-6 overflow-y-auto">
19
+ <div className="max-w-md w-full space-y-6">
22
20
  {/* Logo */}
23
21
  <div className="flex flex-col items-center space-y-4">
24
- <Image
22
+ <img
25
23
  src="/jettypod_wordmark.png"
26
24
  alt="JettyPod"
27
25
  width={160}
28
26
  height={40}
29
- priority
30
27
  />
31
28
  <p className="text-zinc-500 dark:text-zinc-400 text-center">
32
29
  Select a project to get started
@@ -34,46 +31,33 @@ export function WelcomeScreen({
34
31
  </div>
35
32
 
36
33
  {/* Project Buttons */}
37
- <div className="pt-4 space-y-3">
38
- <button
34
+ <div className="pt-4 space-y-4">
35
+ <Button
39
36
  onClick={onNewProject}
40
- className="w-full py-3 px-6 rounded-xl font-medium transition-all duration-200 hover:-translate-y-1 hover:scale-[1.01] active:translate-y-0 active:scale-100"
41
- style={{
42
- cursor: 'pointer',
43
- background: 'linear-gradient(145deg, #ffffff 0%, #faf9f7 10%, #f0f4f4 35%, #c8d9da 55%, #819D9F 90%)',
44
- color: '#3d4d4e',
45
- boxShadow: `
46
- 0 1px 1px rgba(0, 0, 0, 0.02),
47
- 0 2px 4px rgba(0, 0, 0, 0.03),
48
- 0 6px 12px rgba(0, 0, 0, 0.05),
49
- 0 12px 24px rgba(0, 0, 0, 0.06),
50
- 0 20px 40px rgba(129, 157, 159, 0.2),
51
- 0 32px 64px rgba(129, 157, 159, 0.18),
52
- inset 0 2px 4px rgba(255, 255, 255, 1),
53
- inset 0 -2px 4px rgba(129, 157, 159, 0.05)
54
- `,
55
- }}
37
+ size="lg"
38
+ fullWidth
56
39
  data-testid="new-project-button"
57
40
  >
58
41
  New Project
59
- </button>
60
- <button
42
+ </Button>
43
+ <Button
61
44
  onClick={onOpenProject}
62
- className="w-full py-3 px-6 rounded-xl font-medium transition-colors text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 border border-zinc-200 dark:border-zinc-700"
63
- style={{ cursor: 'pointer' }}
45
+ variant="secondary"
46
+ size="lg"
47
+ fullWidth
64
48
  data-testid="open-project-button"
65
49
  >
66
50
  Open Project
67
- </button>
51
+ </Button>
68
52
  </div>
69
53
 
70
54
  {/* Recent Projects Section */}
71
- <div className="pt-8 space-y-4" data-testid="recent-projects-section">
55
+ <div className="pt-6 space-y-3" data-testid="recent-projects-section">
72
56
  <h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
73
57
  Recent Projects
74
58
  </h2>
75
59
  {recentProjects.length === 0 ? (
76
- <div className="border border-zinc-200 dark:border-zinc-700 rounded-lg p-4 text-zinc-500 dark:text-zinc-400 text-sm text-center">
60
+ <div className="rounded-lg p-4 text-zinc-500 dark:text-zinc-400 text-base text-center">
77
61
  No recent projects
78
62
  </div>
79
63
  ) : (
@@ -82,13 +66,13 @@ export function WelcomeScreen({
82
66
  <button
83
67
  key={project.path}
84
68
  onClick={() => onSelectRecentProject?.(project)}
85
- className="w-full text-left p-4 border border-zinc-200 dark:border-zinc-700 rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
69
+ className="w-full text-left p-4 rounded-xl bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors duration-200 ease-out cursor-pointer"
86
70
  data-testid={`recent-project-${project.name}`}
87
71
  >
88
72
  <div className="font-medium text-zinc-900 dark:text-zinc-100">
89
73
  {project.name}
90
74
  </div>
91
- <div className="text-sm text-zinc-500 dark:text-zinc-400 truncate">
75
+ <div className="text-base text-zinc-500 dark:text-zinc-400 truncate">
92
76
  {project.path}
93
77
  </div>
94
78
  </button>
@@ -1,19 +1,18 @@
1
- 'use client';
2
1
 
3
2
  import { CopyableId } from './CopyableId';
3
+ import { TypeIcon } from './TypeIcon';
4
4
 
5
5
  interface WorkItemHeaderProps {
6
6
  id: number;
7
7
  title: string;
8
8
  type: string;
9
- typeIcon: string;
10
9
  typeLabel: string;
11
10
  }
12
11
 
13
- export function WorkItemHeader({ id, title, type, typeIcon, typeLabel }: WorkItemHeaderProps) {
12
+ export function WorkItemHeader({ id, title, type, typeLabel }: WorkItemHeaderProps) {
14
13
  return (
15
- <div className="flex items-center gap-2 text-sm text-zinc-500 mb-1">
16
- <span>{typeIcon} {typeLabel}</span>
14
+ <div className="flex items-center gap-3 text-base text-zinc-500 mb-1.5">
15
+ <span className="flex items-center gap-1"><TypeIcon type={type} /> {typeLabel}</span>
17
16
  <span>•</span>
18
17
  <CopyableId id={id} title={title} type={type} />
19
18
  </div>