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
@@ -2,13 +2,13 @@ import { spawnSync } from 'child_process';
2
2
  import { NextRequest, NextResponse } from 'next/server';
3
3
  import path from 'path';
4
4
  import fs from 'fs';
5
- import { appendSessionContent, getSession, getSessionContent, ConversationTurn } from '@/lib/db';
5
+ import { appendSessionContent, getSessionContent, getSession, createWorkItem, updateWorkItemDescription, ConversationTurn } from '@/lib/db';
6
6
  import {
7
7
  getOrCreateProcess,
8
8
  sendMessage as sendProcessMessage,
9
- hasActiveProcess,
10
9
  killProcess,
11
10
  } from '@/lib/claude-process-manager';
11
+ import { parseBacklogInput } from '@/lib/backlog-parser';
12
12
 
13
13
  /**
14
14
  * Get the project root path for Claude CLI operations.
@@ -30,9 +30,14 @@ function getSettingsPath(projectRoot: string): string | undefined {
30
30
 
31
31
  export const dynamic = 'force-dynamic';
32
32
 
33
+ // Cache the CLI availability check so we only pay the spawnSync cost once per process lifetime
34
+ let claudeCliAvailable: boolean | null = null;
35
+
33
36
  function isClaudeCliAvailable(): boolean {
37
+ if (claudeCliAvailable !== null) return claudeCliAvailable;
34
38
  const result = spawnSync('which', ['claude'], { encoding: 'utf-8' });
35
- return result.status === 0 && result.stdout.trim().length > 0;
39
+ claudeCliAvailable = result.status === 0 && result.stdout.trim().length > 0;
40
+ return claudeCliAvailable;
36
41
  }
37
42
 
38
43
  /**
@@ -59,6 +64,140 @@ function isValidSessionId(id: string): boolean {
59
64
  return !isNaN(parsed) && parsed > 0 && String(parsed) === id;
60
65
  }
61
66
 
67
+ // Backlog question gate data
68
+ const BACKLOG_QUESTION_GATE = JSON.stringify({
69
+ question: 'Done. What now?',
70
+ options: [
71
+ { id: 'finished', label: 'Finished adding to the backlog', description: 'Close this tab' },
72
+ { id: 'add-details', label: 'Add details', description: 'Describe what this work item involves' },
73
+ { id: 'create-another', label: 'Create another work item', description: 'Add another item to the backlog' },
74
+ ],
75
+ });
76
+
77
+ /**
78
+ * Build a synthetic SSE response from an array of text chunks.
79
+ * Each chunk becomes a separate assistant message event.
80
+ */
81
+ function buildSyntheticSSE(chunks: string[]): Response {
82
+ const encoder = new TextEncoder();
83
+ const stream = new ReadableStream({
84
+ start(controller) {
85
+ for (const text of chunks) {
86
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({
87
+ type: 'assistant',
88
+ message: { content: [{ type: 'text', text }] }
89
+ })}\n\n`));
90
+ }
91
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done', exitCode: 0 })}\n\n`));
92
+ controller.close();
93
+ }
94
+ });
95
+ return new Response(stream, {
96
+ headers: {
97
+ 'Content-Type': 'text/event-stream',
98
+ 'Cache-Control': 'no-cache',
99
+ 'Connection': 'keep-alive',
100
+ },
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Handle the full "Add to Backlog" conversation flow without spawning Claude CLI.
106
+ * Returns a Response for handled cases, or null to fall through to Claude CLI.
107
+ */
108
+ function handleBacklogMessage(sessionIdNum: number, message: string): Response | null {
109
+ // Check conversation history to determine state
110
+ const history = getSessionContent(sessionIdNum);
111
+ const lastAssistant = [...history].reverse().find(t => t.role === 'assistant')?.content || '';
112
+
113
+ // Option: "Finished adding to the backlog" — handled client-side (tab closes)
114
+ // This shouldn't normally reach the server, but handle gracefully if it does
115
+ if (message === 'Finished adding to the backlog') {
116
+ appendSessionContent(sessionIdNum, {
117
+ role: 'assistant',
118
+ content: 'Done!',
119
+ timestamp: new Date().toISOString()
120
+ });
121
+ return buildSyntheticSSE(['Done!']);
122
+ }
123
+
124
+ // Option: "Add details" — prompt for details
125
+ if (message === 'Add details') {
126
+ const promptText = 'Tell me more about this work item.';
127
+ appendSessionContent(sessionIdNum, {
128
+ role: 'assistant',
129
+ content: promptText,
130
+ timestamp: new Date().toISOString()
131
+ });
132
+ return buildSyntheticSSE([promptText]);
133
+ }
134
+
135
+ // Option: "Create another work item" — prompt for name
136
+ if (message === 'Create another work item') {
137
+ const promptText = 'What should we call this next one?';
138
+ appendSessionContent(sessionIdNum, {
139
+ role: 'assistant',
140
+ content: promptText,
141
+ timestamp: new Date().toISOString()
142
+ });
143
+ return buildSyntheticSSE([promptText]);
144
+ }
145
+
146
+ // State: user is adding details (previous message was "Tell me more...")
147
+ if (lastAssistant.includes('Tell me more about this work item')) {
148
+ // Find the last created work item ID from history
149
+ const workItemMatch = [...history].reverse()
150
+ .find(t => t.role === 'assistant' && /Created \w+ #(\d+):/.test(t.content))
151
+ ?.content.match(/Created \w+ #(\d+):/);
152
+
153
+ if (workItemMatch) {
154
+ const workItemId = parseInt(workItemMatch[1], 10);
155
+ try {
156
+ updateWorkItemDescription(workItemId, message);
157
+ const confirmText = `Updated description for #${workItemId}.`;
158
+ appendSessionContent(sessionIdNum, {
159
+ role: 'assistant',
160
+ content: confirmText + `\n[GATE:question]${BACKLOG_QUESTION_GATE}[/GATE]`,
161
+ timestamp: new Date().toISOString()
162
+ });
163
+ return buildSyntheticSSE([
164
+ confirmText,
165
+ `[GATE:question]${BACKLOG_QUESTION_GATE}[/GATE]`,
166
+ ]);
167
+ } catch (err) {
168
+ console.error('[backlog] Failed to update description:', err);
169
+ }
170
+ }
171
+ // Fall through to Claude CLI if we can't find the work item
172
+ return null;
173
+ }
174
+
175
+ // Default state: user is providing a work item name — try local parsing
176
+ const parsed = parseBacklogInput(message);
177
+ if (parsed) {
178
+ try {
179
+ const workItem = createWorkItem(parsed.type, parsed.title);
180
+ const confirmText = `Created ${parsed.type} #${workItem.id}: ${parsed.title}`;
181
+ const cardGate = `[GATE:work-item-card]${JSON.stringify({ id: workItem.id, type: workItem.type, title: workItem.title, status: 'backlog' })}[/GATE]`;
182
+ const questionGate = `[GATE:question]${BACKLOG_QUESTION_GATE}[/GATE]`;
183
+
184
+ appendSessionContent(sessionIdNum, {
185
+ role: 'assistant',
186
+ content: confirmText + '\n' + cardGate + '\n' + questionGate,
187
+ timestamp: new Date().toISOString()
188
+ });
189
+
190
+ // Send as separate SSE events so stream manager processes each gate independently
191
+ return buildSyntheticSSE([confirmText, cardGate, questionGate]);
192
+ } catch (err) {
193
+ console.error('[backlog-parser] Failed to create work item:', err);
194
+ }
195
+ }
196
+
197
+ // Can't parse locally — fall through to Claude CLI with haiku
198
+ return null;
199
+ }
200
+
62
201
  export async function POST(
63
202
  request: NextRequest,
64
203
  { params }: { params: Promise<{ sessionId: string }> }
@@ -85,9 +224,9 @@ export async function POST(
85
224
  );
86
225
  }
87
226
 
88
- // Get the message
227
+ // Get the message and optional images
89
228
  const body = await request.json().catch(() => ({}));
90
- const { message } = body;
229
+ const { message, images } = body;
91
230
 
92
231
  if (!message || typeof message !== 'string') {
93
232
  return NextResponse.json(
@@ -108,7 +247,7 @@ export async function POST(
108
247
  });
109
248
 
110
249
  // Return a helpful response without invoking Claude
111
- const clarificationMessage = 'I didn\'t catch that. What would you like to add to the backlog?';
250
+ const clarificationMessage = 'I didn\'t catch that. What should we call this work item?';
112
251
 
113
252
  // Save assistant response to session
114
253
  appendSessionContent(sessionIdNum, {
@@ -149,12 +288,24 @@ export async function POST(
149
288
  timestamp: new Date().toISOString()
150
289
  });
151
290
 
291
+ // Fast backlog creation: handle the full conversation flow locally
292
+ const session = getSession(sessionIdNum);
293
+ if (session?.title === 'Add to Backlog') {
294
+ const backlogResponse = handleBacklogMessage(sessionIdNum, trimmedMessage);
295
+ if (backlogResponse) return backlogResponse;
296
+ // If null, fall through to Claude CLI
297
+ }
298
+
299
+ // Determine model for CLI: backlog sessions use haiku for faster responses
300
+ const isBacklogSession = session?.title === 'Add to Backlog';
301
+ const processOptions = isBacklogSession ? { model: 'haiku' } : undefined;
302
+
152
303
  // Standalone sessions run in main repo (no worktree)
153
304
  const claudeCwd = getProjectRoot();
154
305
  const settingsPath = getSettingsPath(claudeCwd);
155
306
 
156
307
  // Get or create persistent Claude process for this session
157
- const processResult = getOrCreateProcess(sessionId, claudeCwd, settingsPath);
308
+ const processResult = getOrCreateProcess(sessionId, claudeCwd, settingsPath, processOptions);
158
309
 
159
310
  // Check if we hit the process limit
160
311
  if ('error' in processResult) {
@@ -308,7 +459,7 @@ export async function POST(
308
459
  emitter.on('close', onClose);
309
460
 
310
461
  // Send the message to the persistent process (with context prefix if respawned)
311
- let sent = sendProcessMessage(sessionId, messageToSend);
462
+ let sent = sendProcessMessage(sessionId, messageToSend, images);
312
463
 
313
464
  // Bug #1000096: If send failed, the process may have died after getOrCreateProcess
314
465
  // Try to create a fresh process and retry once
@@ -316,7 +467,7 @@ export async function POST(
316
467
  // Kill any zombie process and create fresh one
317
468
  killProcess(sessionId);
318
469
 
319
- const retryResult = getOrCreateProcess(sessionId, claudeCwd, settingsPath);
470
+ const retryResult = getOrCreateProcess(sessionId, claudeCwd, settingsPath, processOptions);
320
471
  if ('error' in retryResult) {
321
472
  // Hit process limit on retry - return error
322
473
  controller.enqueue(encoder.encode(`data: ${JSON.stringify({
@@ -337,7 +488,7 @@ export async function POST(
337
488
  newEmitter.on('close', onClose!);
338
489
 
339
490
  // Retry send (with context prefix since this is also a fresh process)
340
- sent = sendProcessMessage(sessionId, messageToSend);
491
+ sent = sendProcessMessage(sessionId, messageToSend, images);
341
492
  }
342
493
 
343
494
  if (!sent) {
@@ -27,8 +27,16 @@ export async function GET(request: NextRequest) {
27
27
  controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
28
28
  }
29
29
 
30
+ // Hold back test_complete events from the runner — we need to ingest
31
+ // results into SQLite BEFORE the frontend refreshes, otherwise it reads stale data.
32
+ let heldTestComplete: Record<string, unknown> | null = null;
33
+
30
34
  const onEvent = (event: { type: string; [key: string]: unknown }) => {
31
35
  const { type, ...data } = event;
36
+ if (type === 'test_complete') {
37
+ heldTestComplete = data;
38
+ return;
39
+ }
32
40
  send(type, data);
33
41
  };
34
42
 
@@ -38,12 +46,16 @@ export async function GET(request: NextRequest) {
38
46
  : runFeature(featureFile, { onEvent, signal: request.signal });
39
47
 
40
48
  run.then((result: { resultsPath: string }) => {
41
- // Ingest results into SQLite
49
+ // Ingest results into SQLite BEFORE sending test_complete
42
50
  try {
43
51
  ingestCucumberResults(result.resultsPath);
44
52
  } catch {
45
53
  // Non-fatal
46
54
  }
55
+ send('test_complete', heldTestComplete || { status: 'fail' });
56
+ controller.close();
57
+ }).catch(() => {
58
+ send('test_complete', { status: 'fail' });
47
59
  controller.close();
48
60
  });
49
61
  },
@@ -0,0 +1,17 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getWeeklyUsage } from '@/lib/db';
3
+
4
+ export const dynamic = 'force-dynamic';
5
+
6
+ const SAFE_DEFAULT = { used: 0, limit: 20, remaining: 20, allowed: true };
7
+
8
+ export async function GET() {
9
+ try {
10
+ const usage = getWeeklyUsage();
11
+ console.log('[usage] /api/usage responding with', usage);
12
+ return NextResponse.json(usage);
13
+ } catch (err) {
14
+ console.error('[usage] /api/usage ERROR — returning safe default', err);
15
+ return NextResponse.json(SAFE_DEFAULT);
16
+ }
17
+ }
@@ -0,0 +1,35 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getWorkItem } from '@/lib/db';
3
+
4
+ export const dynamic = 'force-dynamic';
5
+
6
+ export async function GET(
7
+ request: NextRequest,
8
+ { params }: { params: Promise<{ id: string }> }
9
+ ) {
10
+ const { id } = await params;
11
+ const workItemId = parseInt(id, 10);
12
+
13
+ const item = getWorkItem(workItemId);
14
+
15
+ if (item) {
16
+ // For chores/bugs with a parent, check the parent's ready_for_review too.
17
+ // The ready_for_review flag is set on the parent feature, not on individual chores.
18
+ let readyForReview = item.ready_for_review;
19
+ if (!readyForReview && item.parent_id) {
20
+ const parent = getWorkItem(item.parent_id);
21
+ if (parent) {
22
+ readyForReview = parent.ready_for_review;
23
+ }
24
+ }
25
+
26
+ return NextResponse.json({
27
+ id: item.id,
28
+ status: item.status,
29
+ ready_for_review: readyForReview,
30
+ rejection_reason: item.rejection_reason,
31
+ });
32
+ } else {
33
+ return NextResponse.json({ error: 'Work item not found' }, { status: 404 });
34
+ }
35
+ }
@@ -1,5 +1,32 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { updateWorkItemStatus } from '@/lib/db';
2
+ import { updateWorkItemStatus, getProjectRoot } from '@/lib/db';
3
+ import path from 'path';
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
6
+ const worktreeFacade = require('../../../../../../../lib/worktree-facade');
7
+
8
+ /**
9
+ * Query for an active worktree for the given work item using better-sqlite3.
10
+ * Returns { id, worktree_path, branch_name } or null.
11
+ */
12
+ function getActiveWorktree(workItemId: number): { id: number; worktree_path: string; branch_name: string } | null {
13
+ const projectRoot = getProjectRoot();
14
+ if (!projectRoot) return null;
15
+
16
+ const dbPath = path.join(projectRoot, '.jettypod', 'work.db');
17
+ try {
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ const Database = require('better-sqlite3');
20
+ const db = new Database(dbPath, { readonly: true });
21
+ const row = db.prepare(
22
+ "SELECT id, worktree_path, branch_name FROM worktrees WHERE work_item_id = ? AND status IN ('active', 'merged')"
23
+ ).get(workItemId) as { id: number; worktree_path: string; branch_name: string } | undefined;
24
+ db.close();
25
+ return row || null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
3
30
 
4
31
  export async function PATCH(
5
32
  request: NextRequest,
@@ -14,6 +41,21 @@ export async function PATCH(
14
41
  const updated = updateWorkItemStatus(workItemId, status, rejectionReason);
15
42
 
16
43
  if (updated) {
44
+ // Auto-cleanup worktree when item is marked done or cancelled
45
+ if (status === 'done' || status === 'cancelled') {
46
+ const worktree = getActiveWorktree(workItemId);
47
+ if (worktree) {
48
+ const projectRoot = getProjectRoot();
49
+ // Fire and forget — don't block the response
50
+ worktreeFacade.stopWork(worktree.id, {
51
+ repoPath: projectRoot,
52
+ deleteBranch: status === 'done',
53
+ }).catch((err: Error) => {
54
+ console.error(`[worktree-cleanup] Failed to cleanup worktree for #${workItemId}: ${err.message}`);
55
+ });
56
+ }
57
+ }
58
+
17
59
  return NextResponse.json({ success: true, id: workItemId, status });
18
60
  } else {
19
61
  return NextResponse.json({ success: false, error: 'Work item not found' }, { status: 404 });
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+
3
+ import { ConnectClaudeScreen } from '@/components/ConnectClaudeScreen';
4
+
5
+ export default function ConnectClaudePage() {
6
+ const handleConnect = async () => {
7
+ if (!window.electronAPI?.isElectron) {
8
+ return { success: false, error: 'Only available in the desktop app.' };
9
+ }
10
+ return await window.electronAPI.claudeCode.login();
11
+ };
12
+
13
+ const handleCheckAuth = async () => {
14
+ if (!window.electronAPI?.isElectron) return false;
15
+ return await window.electronAPI.claudeCode.isAuthenticated();
16
+ };
17
+
18
+ return (
19
+ <ConnectClaudeScreen
20
+ onConnect={handleConnect}
21
+ onCheckAuth={handleCheckAuth}
22
+ />
23
+ );
24
+ }
@@ -24,8 +24,8 @@ export default async function DecisionPage({ params }: PageProps) {
24
24
  <div className="min-h-screen bg-zinc-50 dark:bg-zinc-950">
25
25
  {/* Header */}
26
26
  <header className="border-b border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900">
27
- <div className="max-w-4xl mx-auto px-4 py-4">
28
- <Link href="/" className="text-blue-600 dark:text-blue-400 hover:underline text-sm">
27
+ <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
29
  ← Back to Dashboard
30
30
  </Link>
31
31
  </div>
@@ -34,7 +34,7 @@ export default async function DecisionPage({ params }: PageProps) {
34
34
  <main className="max-w-4xl mx-auto px-4 py-6">
35
35
  {/* Breadcrumb to parent work item */}
36
36
  {decision.work_item_id && (
37
- <div className="mb-4 text-sm text-zinc-500">
37
+ <div className="mb-6 text-base text-zinc-500">
38
38
  <Link href={`/work/${decision.work_item_id}`} className="hover:underline">
39
39
  #{decision.work_item_id} {decision.work_item_title}
40
40
  </Link>
@@ -46,10 +46,10 @@ export default async function DecisionPage({ params }: PageProps) {
46
46
  {/* Main card */}
47
47
  <div className="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 overflow-hidden">
48
48
  {/* Header */}
49
- <div className="px-6 py-4 border-b border-zinc-200 dark:border-zinc-800">
50
- <div className="flex items-start justify-between gap-4">
49
+ <div className="px-8 py-6 border-b border-zinc-200 dark:border-zinc-800">
50
+ <div className="flex items-start justify-between gap-6">
51
51
  <div>
52
- <div className="flex items-center gap-2 text-sm text-zinc-500 mb-1">
52
+ <div className="flex items-center gap-3 text-base text-zinc-500 mb-1.5">
53
53
  <span>📋 Decision</span>
54
54
  <span>•</span>
55
55
  <span className="font-mono">#{decision.id}</span>
@@ -62,8 +62,8 @@ export default async function DecisionPage({ params }: PageProps) {
62
62
  </div>
63
63
 
64
64
  {/* Decision content */}
65
- <div className="px-6 py-4 border-b border-zinc-200 dark:border-zinc-800">
66
- <h2 className="text-sm font-semibold text-zinc-500 uppercase tracking-wide mb-2">
65
+ <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
67
  Decision
68
68
  </h2>
69
69
  <p className="text-zinc-700 dark:text-zinc-300 whitespace-pre-wrap text-lg">
@@ -73,8 +73,8 @@ export default async function DecisionPage({ params }: PageProps) {
73
73
 
74
74
  {/* Rationale */}
75
75
  {decision.rationale && (
76
- <div className="px-6 py-4 border-b border-zinc-200 dark:border-zinc-800">
77
- <h2 className="text-sm font-semibold text-zinc-500 uppercase tracking-wide mb-2">
76
+ <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
78
  Rationale
79
79
  </h2>
80
80
  <p className="text-zinc-700 dark:text-zinc-300 whitespace-pre-wrap">
@@ -84,15 +84,15 @@ export default async function DecisionPage({ params }: PageProps) {
84
84
  )}
85
85
 
86
86
  {/* Metadata */}
87
- <div className="px-6 py-4">
88
- <h2 className="text-sm font-semibold text-zinc-500 uppercase tracking-wide mb-3">
87
+ <div className="px-8 py-6">
88
+ <h2 className="text-base font-semibold text-zinc-500 uppercase tracking-wide mb-4">
89
89
  Details
90
90
  </h2>
91
- <dl className="grid grid-cols-2 gap-4 text-sm">
91
+ <dl className="grid grid-cols-2 gap-6 text-base">
92
92
  <div>
93
93
  <dt className="text-zinc-500">Related Work Item</dt>
94
94
  <dd className="text-zinc-900 dark:text-zinc-100">
95
- <Link href={`/work/${decision.work_item_id}`} className="hover:underline text-blue-600 dark:text-blue-400">
95
+ <Link href={`/work/${decision.work_item_id}`} className="hover:underline text-[#5a7d7f] dark:text-[#a3bfc0]">
96
96
  #{decision.work_item_id} {decision.work_item_title}
97
97
  </Link>
98
98
  </dd>