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.
- package/.env +7 -0
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
- package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
- package/apps/dashboard/app/api/usage/route.ts +17 -0
- package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
- package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
- package/apps/dashboard/app/connect-claude/page.tsx +24 -0
- package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
- package/apps/dashboard/app/demo/gates/page.tsx +42 -42
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +6 -2
- package/apps/dashboard/app/install-claude/page.tsx +9 -7
- package/apps/dashboard/app/layout.tsx +17 -5
- package/apps/dashboard/app/login/page.tsx +250 -0
- package/apps/dashboard/app/page.tsx +11 -9
- package/apps/dashboard/app/settings/page.tsx +4 -2
- package/apps/dashboard/app/signup/page.tsx +245 -0
- package/apps/dashboard/app/subscribe/page.tsx +11 -0
- package/apps/dashboard/app/welcome/page.tsx +24 -1
- package/apps/dashboard/app/work/[id]/page.tsx +34 -50
- package/apps/dashboard/components/AppShell.tsx +95 -55
- package/apps/dashboard/components/CardMenu.tsx +56 -13
- package/apps/dashboard/components/ClaudePanel.tsx +301 -582
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
- package/apps/dashboard/components/CopyableId.tsx +3 -3
- package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
- package/apps/dashboard/components/DragContext.tsx +75 -65
- package/apps/dashboard/components/DraggableCard.tsx +6 -46
- package/apps/dashboard/components/DropZone.tsx +2 -2
- package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
- package/apps/dashboard/components/EditableTitle.tsx +26 -6
- package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
- package/apps/dashboard/components/EpicGroup.tsx +329 -0
- package/apps/dashboard/components/GateCard.tsx +100 -16
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
- package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
- package/apps/dashboard/components/JettyLoader.tsx +38 -0
- package/apps/dashboard/components/KanbanBoard.tsx +147 -766
- package/apps/dashboard/components/KanbanCard.tsx +506 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
- package/apps/dashboard/components/MainNav.tsx +20 -54
- package/apps/dashboard/components/MessageBlock.tsx +391 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -15
- package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
- package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
- package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
- package/apps/dashboard/components/ReviewFooter.tsx +141 -0
- package/apps/dashboard/components/SessionList.tsx +19 -18
- package/apps/dashboard/components/SubscribeContent.tsx +206 -0
- package/apps/dashboard/components/TestTree.tsx +15 -14
- package/apps/dashboard/components/TipCard.tsx +177 -0
- package/apps/dashboard/components/Toast.tsx +5 -5
- package/apps/dashboard/components/TypeIcon.tsx +56 -0
- package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
- package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
- package/apps/dashboard/components/WorkItemTree.tsx +9 -28
- package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
- package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
- package/apps/dashboard/contexts/UsageContext.tsx +155 -0
- package/apps/dashboard/contexts/usageHelpers.js +9 -0
- package/apps/dashboard/electron/ipc-handlers.js +281 -88
- package/apps/dashboard/electron/main.js +691 -131
- package/apps/dashboard/electron/preload.js +25 -4
- package/apps/dashboard/electron/session-manager.js +163 -0
- package/apps/dashboard/electron-builder.config.js +3 -5
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/lib/backlog-parser.ts +50 -0
- package/apps/dashboard/lib/claude-process-manager.ts +50 -11
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/db-bridge.ts +33 -0
- package/apps/dashboard/lib/db.ts +136 -20
- package/apps/dashboard/lib/kanban-utils.ts +70 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/session-state-machine.ts +3 -0
- package/apps/dashboard/lib/session-stream-manager.ts +144 -38
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/tests.ts +3 -1
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next.config.js +35 -14
- package/apps/dashboard/package.json +6 -3
- package/apps/dashboard/public/bug-icon.svg +9 -0
- package/apps/dashboard/public/buoy-icon.svg +9 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.svg +9 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.svg +14 -0
- package/apps/dashboard/public/star-icon.svg +9 -0
- package/apps/dashboard/public/wrench-icon.svg +9 -0
- package/apps/dashboard/scripts/upload-to-r2.js +89 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
- package/apps/update-server/package.json +16 -0
- package/apps/update-server/schema.sql +31 -0
- package/apps/update-server/src/index.ts +1085 -0
- package/apps/update-server/tsconfig.json +16 -0
- package/apps/update-server/wrangler.toml +35 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +54 -116
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/migrations/027-plan-at-creation-column.js +33 -0
- package/lib/migrations/028-ready-for-review-column.js +27 -0
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +13 -6
- package/lib/seed-onboarding.js +101 -69
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +129 -16
- package/lib/work-tracking/index.js +86 -46
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +39 -28
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +131 -68
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/epic-planning/SKILL.md +68 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +83 -73
- package/skills-templates/production-mode/SKILL.md +49 -49
- package/skills-templates/request-routing/SKILL.md +27 -14
- package/skills-templates/simple-improvement/SKILL.md +68 -44
- package/skills-templates/speed-mode/SKILL.md +209 -128
- package/skills-templates/stable-mode/SKILL.md +105 -94
- package/templates/bdd-guidance.md +139 -0
- package/templates/bdd-scaffolding/wait.js +18 -0
- package/templates/bdd-scaffolding/world.js +19 -0
- package/.jettypod-backup/work.db +0 -0
- package/apps/dashboard/app/access-code/page.tsx +0 -110
- package/lib/discovery-checkpoint.js +0 -123
- 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,
|
|
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
|
-
|
|
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
|
|
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-
|
|
28
|
-
<Link href="/" className="text-
|
|
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-
|
|
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-
|
|
50
|
-
<div className="flex items-start justify-between gap-
|
|
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-
|
|
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-
|
|
66
|
-
<h2 className="text-
|
|
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-
|
|
77
|
-
<h2 className="text-
|
|
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-
|
|
88
|
-
<h2 className="text-
|
|
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-
|
|
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-
|
|
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>
|