jettypod 4.4.116 → 4.4.120

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 (162) hide show
  1. package/.env +7 -0
  2. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
  3. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
  4. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
  5. package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
  6. package/apps/dashboard/app/api/usage/route.ts +17 -0
  7. package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
  8. package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
  9. package/apps/dashboard/app/connect-claude/page.tsx +24 -0
  10. package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
  11. package/apps/dashboard/app/demo/gates/page.tsx +42 -42
  12. package/apps/dashboard/app/design-system/page.tsx +868 -0
  13. package/apps/dashboard/app/globals.css +6 -2
  14. package/apps/dashboard/app/install-claude/page.tsx +9 -7
  15. package/apps/dashboard/app/layout.tsx +17 -5
  16. package/apps/dashboard/app/login/page.tsx +250 -0
  17. package/apps/dashboard/app/page.tsx +11 -9
  18. package/apps/dashboard/app/settings/page.tsx +4 -2
  19. package/apps/dashboard/app/signup/page.tsx +245 -0
  20. package/apps/dashboard/app/subscribe/page.tsx +11 -0
  21. package/apps/dashboard/app/welcome/page.tsx +24 -1
  22. package/apps/dashboard/app/work/[id]/page.tsx +34 -50
  23. package/apps/dashboard/components/AppShell.tsx +95 -55
  24. package/apps/dashboard/components/CardMenu.tsx +56 -13
  25. package/apps/dashboard/components/ClaudePanel.tsx +301 -582
  26. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
  27. package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
  28. package/apps/dashboard/components/CopyableId.tsx +3 -3
  29. package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
  30. package/apps/dashboard/components/DragContext.tsx +75 -65
  31. package/apps/dashboard/components/DraggableCard.tsx +6 -46
  32. package/apps/dashboard/components/DropZone.tsx +2 -2
  33. package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
  34. package/apps/dashboard/components/EditableTitle.tsx +26 -6
  35. package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
  36. package/apps/dashboard/components/EpicGroup.tsx +329 -0
  37. package/apps/dashboard/components/GateCard.tsx +100 -16
  38. package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
  39. package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
  40. package/apps/dashboard/components/JettyLoader.tsx +38 -0
  41. package/apps/dashboard/components/KanbanBoard.tsx +147 -766
  42. package/apps/dashboard/components/KanbanCard.tsx +506 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
  44. package/apps/dashboard/components/MainNav.tsx +20 -54
  45. package/apps/dashboard/components/MessageBlock.tsx +391 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -15
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
  53. package/apps/dashboard/components/ReviewFooter.tsx +141 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -18
  55. package/apps/dashboard/components/SubscribeContent.tsx +206 -0
  56. package/apps/dashboard/components/TestTree.tsx +15 -14
  57. package/apps/dashboard/components/TipCard.tsx +177 -0
  58. package/apps/dashboard/components/Toast.tsx +5 -5
  59. package/apps/dashboard/components/TypeIcon.tsx +56 -0
  60. package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
  62. package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
  64. package/apps/dashboard/components/WorkItemTree.tsx +9 -28
  65. package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
  66. package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
  67. package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
  68. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
  69. package/apps/dashboard/components/ui/Button.tsx +104 -0
  70. package/apps/dashboard/components/ui/Input.tsx +78 -0
  71. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
  72. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
  73. package/apps/dashboard/contexts/UsageContext.tsx +155 -0
  74. package/apps/dashboard/contexts/usageHelpers.js +9 -0
  75. package/apps/dashboard/electron/ipc-handlers.js +281 -88
  76. package/apps/dashboard/electron/main.js +691 -131
  77. package/apps/dashboard/electron/preload.js +25 -4
  78. package/apps/dashboard/electron/session-manager.js +163 -0
  79. package/apps/dashboard/electron-builder.config.js +3 -5
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/lib/backlog-parser.ts +50 -0
  83. package/apps/dashboard/lib/claude-process-manager.ts +50 -11
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/db-bridge.ts +33 -0
  86. package/apps/dashboard/lib/db.ts +136 -20
  87. package/apps/dashboard/lib/kanban-utils.ts +70 -0
  88. package/apps/dashboard/lib/run-migrations.js +27 -2
  89. package/apps/dashboard/lib/session-state-machine.ts +3 -0
  90. package/apps/dashboard/lib/session-stream-manager.ts +144 -38
  91. package/apps/dashboard/lib/shadows.ts +7 -0
  92. package/apps/dashboard/lib/tests.ts +3 -1
  93. package/apps/dashboard/lib/utils.ts +6 -0
  94. package/apps/dashboard/next.config.js +35 -14
  95. package/apps/dashboard/package.json +6 -3
  96. package/apps/dashboard/public/bug-icon.svg +9 -0
  97. package/apps/dashboard/public/buoy-icon.svg +9 -0
  98. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  99. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  100. package/apps/dashboard/public/in-flight-seagull.svg +9 -0
  101. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  102. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  103. package/apps/dashboard/public/jettypod_logo.png +0 -0
  104. package/apps/dashboard/public/pier-icon.svg +14 -0
  105. package/apps/dashboard/public/star-icon.svg +9 -0
  106. package/apps/dashboard/public/wrench-icon.svg +9 -0
  107. package/apps/dashboard/scripts/upload-to-r2.js +89 -0
  108. package/apps/dashboard/scripts/ws-server.js +191 -0
  109. package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
  110. package/apps/update-server/package.json +16 -0
  111. package/apps/update-server/schema.sql +31 -0
  112. package/apps/update-server/src/index.ts +1085 -0
  113. package/apps/update-server/tsconfig.json +16 -0
  114. package/apps/update-server/wrangler.toml +35 -0
  115. package/cucumber.js +9 -3
  116. package/docs/COMMAND_REFERENCE.md +34 -0
  117. package/hooks/post-checkout +32 -75
  118. package/hooks/post-merge +111 -10
  119. package/jest.setup.js +1 -0
  120. package/jettypod.js +54 -116
  121. package/lib/chore-taxonomy.js +33 -10
  122. package/lib/database.js +36 -16
  123. package/lib/db-watcher.js +1 -1
  124. package/lib/git-hooks/pre-commit +1 -1
  125. package/lib/jettypod-backup.js +27 -4
  126. package/lib/migrations/027-plan-at-creation-column.js +33 -0
  127. package/lib/migrations/028-ready-for-review-column.js +27 -0
  128. package/lib/migrations/029-remove-autoincrement.js +307 -0
  129. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  130. package/lib/migrations/index.js +47 -4
  131. package/lib/schema.js +13 -6
  132. package/lib/seed-onboarding.js +101 -69
  133. package/lib/update-command/index.js +9 -175
  134. package/lib/work-commands/index.js +129 -16
  135. package/lib/work-tracking/index.js +86 -46
  136. package/lib/worktree-diagnostics.js +16 -16
  137. package/lib/worktree-facade.js +1 -1
  138. package/lib/worktree-manager.js +8 -8
  139. package/lib/worktree-reconciler.js +5 -5
  140. package/package.json +9 -2
  141. package/scripts/ndjson-to-cucumber-json.js +152 -0
  142. package/scripts/postinstall.js +25 -0
  143. package/skills-templates/bug-mode/SKILL.md +39 -28
  144. package/skills-templates/bug-planning/SKILL.md +25 -29
  145. package/skills-templates/chore-mode/SKILL.md +131 -68
  146. package/skills-templates/chore-mode/verification.js +51 -10
  147. package/skills-templates/chore-planning/SKILL.md +47 -18
  148. package/skills-templates/epic-planning/SKILL.md +68 -48
  149. package/skills-templates/external-transition/SKILL.md +47 -47
  150. package/skills-templates/feature-planning/SKILL.md +83 -73
  151. package/skills-templates/production-mode/SKILL.md +49 -49
  152. package/skills-templates/request-routing/SKILL.md +27 -14
  153. package/skills-templates/simple-improvement/SKILL.md +68 -44
  154. package/skills-templates/speed-mode/SKILL.md +209 -128
  155. package/skills-templates/stable-mode/SKILL.md +105 -94
  156. package/templates/bdd-guidance.md +139 -0
  157. package/templates/bdd-scaffolding/wait.js +18 -0
  158. package/templates/bdd-scaffolding/world.js +19 -0
  159. package/.jettypod-backup/work.db +0 -0
  160. package/apps/dashboard/app/access-code/page.tsx +0 -110
  161. package/lib/discovery-checkpoint.js +0 -123
  162. package/skills-templates/project-discovery/SKILL.md +0 -372
@@ -0,0 +1,11 @@
1
+ 'use client';
2
+
3
+ import { SubscribeContent } from '@/components/SubscribeContent';
4
+
5
+ export default function SubscribePage() {
6
+ return (
7
+ <div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900 p-8">
8
+ <SubscribeContent />
9
+ </div>
10
+ );
11
+ }
@@ -22,6 +22,28 @@ export default function WelcomePage() {
22
22
  loadRecentProjects();
23
23
  }, []);
24
24
 
25
+ const handleNewProject = async () => {
26
+ setError(null);
27
+
28
+ if (!window.electronAPI?.isElectron) {
29
+ setError('Project creation is only available in the desktop app.');
30
+ return;
31
+ }
32
+
33
+ const result = await window.electronAPI.project.newProject();
34
+
35
+ if (result.canceled) {
36
+ return;
37
+ }
38
+
39
+ if (!result.success) {
40
+ setError(result.error || 'Failed to create project');
41
+ return;
42
+ }
43
+
44
+ window.location.href = '/';
45
+ };
46
+
25
47
  const handleOpenProject = async () => {
26
48
  setError(null);
27
49
 
@@ -70,12 +92,13 @@ export default function WelcomePage() {
70
92
  return (
71
93
  <>
72
94
  {error && (
73
- <div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded z-50">
95
+ <div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-red-50 dark:bg-red-900/20 border-2 border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded-xl text-base z-50">
74
96
  {error}
75
97
  </div>
76
98
  )}
77
99
  <WelcomeScreen
78
100
  recentProjects={recentProjects}
101
+ onNewProject={handleNewProject}
79
102
  onOpenProject={handleOpenProject}
80
103
  onSelectRecentProject={handleSelectRecentProject}
81
104
  />
@@ -5,27 +5,9 @@ import { EditableDetailTitle } from '@/components/EditableDetailTitle';
5
5
  import { EditableDetailDescription } from '@/components/EditableDetailDescription';
6
6
  import Link from 'next/link';
7
7
  import { notFound } from 'next/navigation';
8
-
9
- const typeLabels: Record<string, { icon: string; label: string }> = {
10
- epic: { icon: '🎯', label: 'Epic' },
11
- feature: { icon: '✨', label: 'Feature' },
12
- chore: { icon: '🔧', label: 'Chore' },
13
- bug: { icon: '🐛', label: 'Bug' },
14
- };
15
-
16
- const statusLabels: Record<string, { label: string; color: string }> = {
17
- backlog: { label: 'Backlog', color: 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300' },
18
- todo: { label: 'Todo', color: 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300' },
19
- in_progress: { label: 'In Progress', color: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300' },
20
- done: { label: 'Done', color: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300' },
21
- cancelled: { label: 'Cancelled', color: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300' },
22
- };
23
-
24
- const modeLabels: Record<string, { label: string; color: string }> = {
25
- speed: { label: 'Speed Mode', color: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200' },
26
- stable: { label: 'Stable Mode', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' },
27
- production: { label: 'Production Mode', color: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200' },
28
- };
8
+ import { TYPE_LABELS, STATUS_LABELS, MODE_LABELS_FULL } from '@/lib/constants';
9
+ import { TypeIcon } from '@/components/TypeIcon';
10
+ import { DetailReviewActions } from '@/components/DetailReviewActions';
29
11
 
30
12
  interface PageProps {
31
13
  params: Promise<{ id: string }>;
@@ -49,15 +31,15 @@ export default async function WorkItemPage({ params }: PageProps) {
49
31
  const decisions = getDecisionsForWorkItem(workItemId);
50
32
  const parentItem = item.parent_id ? getWorkItem(item.parent_id) : null;
51
33
 
52
- const typeInfo = typeLabels[item.type] || { icon: '📄', label: 'Item' };
53
- const statusInfo = statusLabels[item.status] || statusLabels.backlog;
34
+ const typeInfo = TYPE_LABELS[item.type] || { icon: '📄', label: 'Item' };
35
+ const statusInfo = STATUS_LABELS[item.status] || STATUS_LABELS.backlog;
54
36
 
55
37
  return (
56
38
  <div className="min-h-screen bg-zinc-50 dark:bg-zinc-950">
57
39
  {/* Header */}
58
40
  <header className="border-b border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900">
59
- <div className="max-w-4xl mx-auto px-4 py-4">
60
- <Link href="/" className="text-blue-600 dark:text-blue-400 hover:underline text-sm">
41
+ <div className="max-w-4xl mx-auto px-6 py-6">
42
+ <Link href="/" className="text-[#5a7d7f] dark:text-[#a3bfc0] hover:underline text-base">
61
43
  ← Back to Dashboard
62
44
  </Link>
63
45
  </div>
@@ -66,9 +48,9 @@ export default async function WorkItemPage({ params }: PageProps) {
66
48
  <main className="max-w-4xl mx-auto px-4 py-6">
67
49
  {/* Breadcrumb */}
68
50
  {parentItem && (
69
- <div className="mb-4 text-sm text-zinc-500">
70
- <Link href={`/work/${parentItem.id}`} className="hover:underline">
71
- {typeLabels[parentItem.type]?.icon} #{parentItem.id} {parentItem.title}
51
+ <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">
53
+ <TypeIcon type={parentItem.type} className="w-6 h-6" /> #{parentItem.id} {parentItem.title}
72
54
  </Link>
73
55
  <span className="mx-2">→</span>
74
56
  </div>
@@ -77,45 +59,47 @@ export default async function WorkItemPage({ params }: PageProps) {
77
59
  {/* Main card */}
78
60
  <div className="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 overflow-hidden">
79
61
  {/* Header */}
80
- <div className="px-6 py-4 border-b border-zinc-200 dark:border-zinc-800">
81
- <div className="flex items-start justify-between gap-4">
62
+ <div className="px-8 py-6 border-b border-zinc-200 dark:border-zinc-800">
63
+ <div className="flex items-start justify-between gap-6">
82
64
  <div className="flex-1 min-w-0">
83
65
  <WorkItemHeader
84
66
  id={item.id}
85
67
  title={item.title}
86
68
  type={item.type}
87
- typeIcon={typeInfo.icon}
88
69
  typeLabel={typeInfo.label}
89
70
  />
90
71
  <EditableDetailTitle title={item.title} itemId={item.id} />
91
72
  </div>
92
73
  <div className="flex items-center gap-2">
93
- {item.mode && modeLabels[item.mode] && (
94
- <span className={`text-sm px-2 py-1 rounded ${modeLabels[item.mode].color}`}>
95
- {modeLabels[item.mode].label}
74
+ {item.mode && MODE_LABELS_FULL[item.mode] && (
75
+ <span className={`text-base px-3 py-1.5 rounded ${MODE_LABELS_FULL[item.mode].color}`}>
76
+ {MODE_LABELS_FULL[item.mode].label}
96
77
  </span>
97
78
  )}
98
- <span className={`text-sm px-2 py-1 rounded ${statusInfo.color}`}>
79
+ <span className={`text-base px-3 py-1.5 rounded ${statusInfo.color}`}>
99
80
  {statusInfo.label}
100
81
  </span>
82
+ {!!item.ready_for_review && (
83
+ <DetailReviewActions workItemId={item.id} />
84
+ )}
101
85
  </div>
102
86
  </div>
103
87
  </div>
104
88
 
105
89
  {/* Description */}
106
- <div className="px-6 py-4 border-b border-zinc-200 dark:border-zinc-800">
107
- <h2 className="text-sm font-semibold text-zinc-500 uppercase tracking-wide mb-2">
90
+ <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">
108
92
  Description
109
93
  </h2>
110
94
  <EditableDetailDescription description={item.description} itemId={item.id} />
111
95
  </div>
112
96
 
113
97
  {/* Metadata */}
114
- <div className="px-6 py-4 border-b border-zinc-200 dark:border-zinc-800">
115
- <h2 className="text-sm font-semibold text-zinc-500 uppercase tracking-wide mb-3">
98
+ <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">
116
100
  Details
117
101
  </h2>
118
- <dl className="grid grid-cols-2 gap-4 text-sm">
102
+ <dl className="grid grid-cols-2 gap-6 text-base">
119
103
  {item.branch_name && (
120
104
  <div>
121
105
  <dt className="text-zinc-500">Branch</dt>
@@ -147,18 +131,18 @@ export default async function WorkItemPage({ params }: PageProps) {
147
131
 
148
132
  {/* Decisions */}
149
133
  {decisions.length > 0 && (
150
- <div className="px-6 py-4 border-b border-zinc-200 dark:border-zinc-800">
151
- <h2 className="text-sm font-semibold text-zinc-500 uppercase tracking-wide mb-3">
134
+ <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">
152
136
  Decisions
153
137
  </h2>
154
- <div className="space-y-4">
138
+ <div className="space-y-6">
155
139
  {decisions.map((decision) => (
156
- <div key={decision.id} className="bg-zinc-50 dark:bg-zinc-800 rounded-lg p-4">
157
- <div className="flex items-center gap-2 mb-2">
158
- <span className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
140
+ <div key={decision.id} className="bg-zinc-50 dark:bg-zinc-800 rounded-lg p-6">
141
+ <div className="flex items-center gap-3 mb-3">
142
+ <span className="text-base font-medium text-zinc-900 dark:text-zinc-100">
159
143
  {decision.aspect}
160
144
  </span>
161
- <span className="text-xs text-zinc-500">
145
+ <span className="text-base text-zinc-500">
162
146
  {new Date(decision.created_at).toLocaleDateString()}
163
147
  </span>
164
148
  </div>
@@ -166,7 +150,7 @@ export default async function WorkItemPage({ params }: PageProps) {
166
150
  {decision.decision}
167
151
  </p>
168
152
  {decision.rationale && (
169
- <p className="text-sm text-zinc-500 mt-1">
153
+ <p className="text-base text-zinc-500 mt-1.5">
170
154
  {decision.rationale}
171
155
  </p>
172
156
  )}
@@ -178,8 +162,8 @@ export default async function WorkItemPage({ params }: PageProps) {
178
162
 
179
163
  {/* Children */}
180
164
  {children.length > 0 && (
181
- <div className="px-6 py-4">
182
- <h2 className="text-sm font-semibold text-zinc-500 uppercase tracking-wide mb-3">
165
+ <div className="px-8 py-6">
166
+ <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-4">
183
167
  {item.type === 'epic' ? 'Features & Chores' : 'Child Items'} ({children.length})
184
168
  </h2>
185
169
  <WorkItemTree items={children.map(c => ({ ...c, children: [] }))} />
@@ -1,15 +1,22 @@
1
1
  'use client';
2
2
 
3
- import { usePathname } from 'next/navigation';
4
- import { ClaudeSessionProvider, useClaudeSession } from '../contexts/ClaudeSessionContext';
5
- import { ConnectionStatusProvider } from '../contexts/ConnectionStatusContext';
3
+ import { useState, useEffect } from 'react';
4
+ import { usePathname, useRouter } from 'next/navigation';
5
+ import { ClaudeSessionProvider, useSessionState, useSessionActions } from '../contexts/ClaudeSessionContext';
6
+ import { ConnectionStatusProvider, useConnectionStatus } from '../contexts/ConnectionStatusContext';
7
+ import { UsageProvider } from '../contexts/UsageContext';
6
8
  import { ToastProvider } from './Toast';
7
9
  import { MainNav } from './MainNav';
8
10
  import { ClaudePanel } from './ClaudePanel';
11
+ import { JettyLoader } from './JettyLoader';
12
+ import { LazyMotion, domAnimation, m, AnimatePresence } from 'framer-motion';
9
13
  import type { ReactNode } from 'react';
10
14
 
11
15
  // Pages that should not show the nav header (pre-project screens)
12
- const NO_NAV_PATHS = ['/install-claude', '/welcome'];
16
+ const NO_NAV_PATHS = ['/login', '/signup', '/subscribe', '/install-claude', '/connect-claude', '/welcome'];
17
+
18
+ // Pages accessible without authentication
19
+ const PUBLIC_PATHS = ['/login', '/signup', '/subscribe', '/install-claude', '/connect-claude', '/welcome', '/design-system'];
13
20
 
14
21
  interface AppShellProps {
15
22
  projectName: string;
@@ -18,74 +25,107 @@ interface AppShellProps {
18
25
 
19
26
  function AppShellContent({ projectName, children }: AppShellProps) {
20
27
  const pathname = usePathname();
28
+ const router = useRouter();
21
29
  const showNav = !NO_NAV_PATHS.includes(pathname);
30
+ const [authChecked, setAuthChecked] = useState(false);
31
+ const [minLoadingDone, setMinLoadingDone] = useState(false);
32
+ const { showDisconnected, status: connectionStatus } = useConnectionStatus();
33
+
34
+ // Ensure the loading screen shows for at least 3 seconds
35
+ useEffect(() => {
36
+ const timer = setTimeout(() => setMinLoadingDone(true), 3000);
37
+ return () => clearTimeout(timer);
38
+ }, []);
39
+
40
+ // Auth enforcement gate: redirect unauthenticated users to /signup (first-time) or /login (returning)
41
+ useEffect(() => {
42
+ if (PUBLIC_PATHS.includes(pathname)) {
43
+ setAuthChecked(true);
44
+ return;
45
+ }
46
+
47
+ async function checkAuth() {
48
+ if (typeof window !== 'undefined' && window.electronAPI?.isElectron) {
49
+ try {
50
+ const status = await window.electronAPI.auth.getStatus();
51
+ if (!status.authenticated) {
52
+ const hasLoggedIn = await window.electronAPI.auth.hasLoggedInBefore?.();
53
+ router.push(hasLoggedIn ? '/login' : '/signup');
54
+ return;
55
+ }
56
+ } catch {
57
+ // Corrupted auth file, IPC error, etc. — treat as unauthenticated
58
+ router.push('/login');
59
+ return;
60
+ }
61
+ }
62
+ setAuthChecked(true);
63
+ }
64
+ checkAuth();
65
+ }, [pathname, router]);
22
66
 
23
- const {
24
- claudePanelOpen,
25
- setClaudePanelOpen,
26
- activeSessionId,
27
- activeSession,
28
- sessions,
29
- standaloneSessions,
30
- messages,
31
- status,
32
- error,
33
- exitCode,
34
- canRetry,
35
- switchSession,
36
- closeSession,
37
- createNewSession,
38
- sendMessage,
39
- retry,
40
- stop,
41
- narratedMode,
42
- toggleNarratedMode,
43
- } = useClaudeSession();
67
+ const { claudePanelOpen } = useSessionState();
68
+ const { setClaudePanelOpen } = useSessionActions();
69
+
70
+ const showLoading = !authChecked || !minLoadingDone;
44
71
 
45
72
  return (
46
- <div className="h-screen flex flex-col overflow-hidden">
73
+ <AnimatePresence mode="wait">
74
+ {showLoading ? (
75
+ <m.div
76
+ key="loader"
77
+ initial={{ opacity: 0 }}
78
+ animate={{ opacity: 1 }}
79
+ exit={{ opacity: 0 }}
80
+ transition={{ duration: 0.3 }}
81
+ className="h-screen flex items-center justify-center bg-background"
82
+ >
83
+ <JettyLoader size={80} />
84
+ </m.div>
85
+ ) : (
86
+ <m.div
87
+ key="content"
88
+ initial={{ opacity: 0 }}
89
+ animate={{ opacity: 1 }}
90
+ transition={{ duration: 0.3 }}
91
+ className="h-screen flex flex-col overflow-hidden">
47
92
  {showNav && <MainNav projectName={projectName} />}
48
- <main className={`flex-1 flex flex-col min-h-0 overflow-y-auto transition-[margin] duration-300 ${showNav && claudePanelOpen ? 'mr-[480px]' : ''}`}>
93
+ {showNav && showDisconnected && (
94
+ <div className="bg-amber-50 dark:bg-amber-950 border-b border-amber-200 dark:border-amber-800 px-4 py-2 text-center text-base text-amber-800 dark:text-amber-200" data-testid="disconnect-banner">
95
+ {connectionStatus === 'reconnecting'
96
+ ? 'Reconnecting to live updates...'
97
+ : 'Live updates disconnected. Changes may not appear automatically.'}
98
+ </div>
99
+ )}
100
+ <main className={`flex-1 flex flex-col min-h-0 overflow-y-auto transition-[margin] duration-200 ease-out ${showNav && claudePanelOpen ? 'mr-[480px]' : ''}`}>
49
101
  {children}
50
102
  </main>
51
103
  {showNav && (
52
104
  <ClaudePanel
53
105
  isOpen={claudePanelOpen}
54
- workItemId={activeSessionId || 'sessions'}
55
- workItemTitle={activeSession?.title || 'Claude Sessions'}
56
- messages={messages}
57
- status={status}
58
- error={error}
59
- exitCode={exitCode}
60
- canRetry={canRetry}
61
106
  onClose={() => setClaudePanelOpen(false)}
62
- onRetry={retry}
63
- onSendMessage={sendMessage}
64
- onStop={stop}
65
- sessions={sessions}
66
- activeSessionId={activeSessionId}
67
- onSwitchSession={switchSession}
68
- standaloneSessions={standaloneSessions}
69
- onNewSession={createNewSession}
70
- onCloseSession={closeSession}
71
- narratedMode={narratedMode}
72
- onToggleNarratedMode={toggleNarratedMode}
73
107
  />
74
108
  )}
75
- </div>
109
+ </m.div>
110
+ )}
111
+ </AnimatePresence>
76
112
  );
77
113
  }
78
114
 
79
115
  export function AppShell({ projectName, children }: AppShellProps) {
80
116
  return (
81
- <ConnectionStatusProvider>
82
- <ToastProvider>
83
- <ClaudeSessionProvider>
84
- <AppShellContent projectName={projectName}>
85
- {children}
86
- </AppShellContent>
87
- </ClaudeSessionProvider>
88
- </ToastProvider>
89
- </ConnectionStatusProvider>
117
+ <LazyMotion features={domAnimation} strict>
118
+ <ConnectionStatusProvider>
119
+ <UsageProvider>
120
+ <ToastProvider>
121
+ <ClaudeSessionProvider>
122
+ <AppShellContent projectName={projectName}>
123
+ {children}
124
+ </AppShellContent>
125
+ </ClaudeSessionProvider>
126
+ </ToastProvider>
127
+ </UsageProvider>
128
+ </ConnectionStatusProvider>
129
+ </LazyMotion>
90
130
  );
91
131
  }
@@ -7,14 +7,19 @@ interface CardMenuProps {
7
7
  itemId: number;
8
8
  itemTitle?: string;
9
9
  itemType?: string;
10
+ itemDescription?: string | null;
11
+ conversational?: boolean;
10
12
  currentStatus: string;
11
13
  onStatusChange: (id: number, newStatus: string) => Promise<void>;
12
- onTriggerClaude?: (id: number, title: string, type: string) => void;
14
+ onTriggerClaude?: (id: number, title: string, type: string, conversational?: boolean, description?: string | null) => void;
13
15
  hasActiveSession?: boolean;
14
16
  onOpenSession?: (id: string) => void;
17
+ usageAllowed?: boolean;
18
+ onEditName?: () => void;
19
+ onUnaccept?: () => void;
15
20
  }
16
21
 
17
- export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentStatus, onStatusChange, onTriggerClaude, hasActiveSession, onOpenSession }: CardMenuProps) {
22
+ export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', itemDescription, conversational = false, currentStatus, onStatusChange, onTriggerClaude, hasActiveSession, onOpenSession, usageAllowed = true, onEditName, onUnaccept }: CardMenuProps) {
18
23
  const [isOpen, setIsOpen] = useState(false);
19
24
  const [error, setError] = useState<string | null>(null);
20
25
  const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number } | null>(null);
@@ -74,7 +79,7 @@ export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentSt
74
79
  await onStatusChange(itemId, 'in_progress');
75
80
  // Trigger Claude panel after status change succeeds
76
81
  if (onTriggerClaude) {
77
- onTriggerClaude(itemId, itemTitle, itemType);
82
+ onTriggerClaude(itemId, itemTitle, itemType, conversational, itemDescription);
78
83
  }
79
84
  } catch (err) {
80
85
  setError(err instanceof Error ? err.message : 'Failed to update status');
@@ -89,40 +94,68 @@ export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentSt
89
94
  }
90
95
  };
91
96
 
97
+ const handleEditName = (e: React.MouseEvent) => {
98
+ e.stopPropagation();
99
+ setIsOpen(false);
100
+ onEditName?.();
101
+ };
102
+
103
+ const handleUnaccept = (e: React.MouseEvent) => {
104
+ e.stopPropagation();
105
+ setIsOpen(false);
106
+ onUnaccept?.();
107
+ };
108
+
92
109
  const dropdownContent = (
93
110
  <div
94
111
  ref={dropdownRef}
95
- className="fixed z-50 bg-white dark:bg-zinc-800 rounded-lg shadow-lg border border-zinc-200 dark:border-zinc-700 py-1 min-w-[140px]"
112
+ className="fixed z-50 bg-white dark:bg-zinc-800 rounded-lg shadow-lg py-1.5 min-w-[140px]"
96
113
  style={{
97
114
  top: dropdownPosition?.top ?? 0,
98
115
  left: dropdownPosition?.left ?? 0,
99
116
  }}
100
117
  data-testid="status-dropdown"
101
118
  >
119
+ {onEditName && (
120
+ <button
121
+ onClick={handleEditName}
122
+ className="w-full px-4 py-3 text-left text-base text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-3"
123
+ data-testid="edit-name-button"
124
+ >
125
+ <span className="text-zinc-500">✏️</span>
126
+ Edit name
127
+ </button>
128
+ )}
102
129
  {(currentStatus === 'backlog' || currentStatus === 'cancelled') && (
103
130
  <button
104
131
  onClick={handleStart}
105
- className="w-full px-3 py-2 text-left text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-2"
132
+ disabled={!usageAllowed}
133
+ className={`w-full px-4 py-3 text-left text-base flex items-center gap-3 ${
134
+ usageAllowed
135
+ ? 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700'
136
+ : 'text-zinc-400 dark:text-zinc-600 cursor-not-allowed'
137
+ }`}
106
138
  data-testid="start-button"
139
+ title={!usageAllowed ? 'Weekly usage limit reached' : undefined}
107
140
  >
108
- <span className="text-blue-500">▶</span>
141
+ <span className={usageAllowed ? 'text-[#819D9F]' : 'text-zinc-400 dark:text-zinc-600'}>▶</span>
109
142
  Start
110
143
  </button>
111
144
  )}
112
145
  {hasActiveSession && (
113
146
  <button
114
147
  onClick={handleOpenSession}
115
- className="w-full px-3 py-2 text-left text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-2"
148
+ className="w-full px-4 py-3 text-left text-base text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-3"
116
149
  data-testid="open-session-button"
117
150
  >
118
- <span className="text-blue-500">💬</span>
151
+ <span className="text-[#819D9F]">💬</span>
119
152
  Open session
120
153
  </button>
121
154
  )}
122
155
  {currentStatus !== 'done' && (
123
156
  <button
124
157
  onClick={(e) => handleAction(e, 'done')}
125
- className="w-full px-3 py-2 text-left text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-2"
158
+ className="w-full px-4 py-3 text-left text-base text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-3"
126
159
  data-testid="mark-done-button"
127
160
  >
128
161
  <span className="text-green-500">✓</span>
@@ -132,17 +165,27 @@ export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentSt
132
165
  {(currentStatus === 'in_progress' || currentStatus === 'done') && (
133
166
  <button
134
167
  onClick={(e) => handleAction(e, 'backlog')}
135
- className="w-full px-3 py-2 text-left text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-2"
168
+ className="w-full px-4 py-3 text-left text-base text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-3"
136
169
  data-testid="unstart-button"
137
170
  >
138
171
  <span className="text-amber-500">↩</span>
139
172
  Unstart
140
173
  </button>
141
174
  )}
175
+ {currentStatus === 'done' && onUnaccept && (
176
+ <button
177
+ onClick={handleUnaccept}
178
+ className="w-full px-4 py-3 text-left text-base text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-3"
179
+ data-testid="unaccept-button"
180
+ >
181
+ <span className="text-red-500">↩</span>
182
+ Unaccept
183
+ </button>
184
+ )}
142
185
  {currentStatus !== 'cancelled' && (
143
186
  <button
144
187
  onClick={(e) => handleAction(e, 'cancelled')}
145
- className="w-full px-3 py-2 text-left text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-2"
188
+ className="w-full px-4 py-3 text-left text-base text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-3"
146
189
  data-testid="cancel-button"
147
190
  >
148
191
  <span className="text-red-500">✕</span>
@@ -157,7 +200,7 @@ export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentSt
157
200
  <button
158
201
  ref={buttonRef}
159
202
  onClick={handleMenuClick}
160
- className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
203
+ className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors duration-200 ease-out"
161
204
  aria-label="Card menu"
162
205
  data-testid="menu-button"
163
206
  >
@@ -172,7 +215,7 @@ export function CardMenu({ itemId, itemTitle = '', itemType = 'chore', currentSt
172
215
 
173
216
  {error && (
174
217
  <div
175
- className="absolute right-0 top-full mt-1 z-10 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-xs px-2 py-1 rounded border border-red-200 dark:border-red-800"
218
+ className="absolute right-0 top-full mt-1 z-10 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-xs px-3 py-1.5 rounded border-2 border-red-200 dark:border-red-800"
176
219
  data-testid="error-message"
177
220
  >
178
221
  {error}