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
package/apps/dashboard/lib/db.ts
CHANGED
|
@@ -26,8 +26,10 @@ export interface WorkItem {
|
|
|
26
26
|
completed_at: string | null;
|
|
27
27
|
rejection_reason: string | null;
|
|
28
28
|
rejected_at: string | null;
|
|
29
|
+
ready_for_review: number;
|
|
29
30
|
created_at: string;
|
|
30
31
|
display_order: number | null;
|
|
32
|
+
conversational: number;
|
|
31
33
|
children?: WorkItem[];
|
|
32
34
|
chores?: WorkItem[];
|
|
33
35
|
bugs?: WorkItem[];
|
|
@@ -44,7 +46,7 @@ export interface Decision {
|
|
|
44
46
|
created_at: string;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
function getProjectRoot(): string | null {
|
|
49
|
+
export function getProjectRoot(): string | null {
|
|
48
50
|
// Use JETTYPOD_PROJECT_PATH if set (passed by Electron main process)
|
|
49
51
|
if (process.env.JETTYPOD_PROJECT_PATH) {
|
|
50
52
|
return process.env.JETTYPOD_PROJECT_PATH;
|
|
@@ -108,6 +110,9 @@ function ensureColumns(db: Database.Database): void {
|
|
|
108
110
|
tryAddTo('work_items', 'display_order', 'INTEGER DEFAULT NULL');
|
|
109
111
|
tryAddTo('work_items', 'rejection_reason', 'TEXT');
|
|
110
112
|
tryAddTo('work_items', 'rejected_at', 'TEXT');
|
|
113
|
+
tryAddTo('work_items', 'conversational', 'INTEGER DEFAULT 0');
|
|
114
|
+
tryAddTo('work_items', 'plan_at_creation', 'TEXT DEFAULT NULL');
|
|
115
|
+
tryAddTo('work_items', 'ready_for_review', 'INTEGER DEFAULT 0');
|
|
111
116
|
} catch {
|
|
112
117
|
// Table might not exist yet - ensureSchema will handle it
|
|
113
118
|
}
|
|
@@ -140,7 +145,7 @@ function ensureColumns(db: Database.Database): void {
|
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
// Check if a project folder has no implementation code
|
|
143
|
-
function isBlankProject(projectPath: string): boolean {
|
|
148
|
+
export function isBlankProject(projectPath: string): boolean {
|
|
144
149
|
const IMPL_PATTERNS = [
|
|
145
150
|
/\.js$/, /\.ts$/, /\.jsx$/, /\.tsx$/, /\.mjs$/, /\.cjs$/,
|
|
146
151
|
/\.py$/, /\.rb$/, /\.go$/, /\.rs$/, /\.java$/, /\.kt$/,
|
|
@@ -171,33 +176,48 @@ function isBlankProject(projectPath: string): boolean {
|
|
|
171
176
|
return !scan(projectPath, projectPath);
|
|
172
177
|
}
|
|
173
178
|
|
|
179
|
+
// Check if any onboarding chore has been started (moved out of backlog)
|
|
180
|
+
export function hasOnboardingStarted(): boolean {
|
|
181
|
+
try {
|
|
182
|
+
const db = getDb();
|
|
183
|
+
const result = db.prepare(`
|
|
184
|
+
SELECT COUNT(*) as count FROM work_items
|
|
185
|
+
WHERE parent_id = (SELECT id FROM work_items WHERE type = 'epic' AND title = 'Project Planning')
|
|
186
|
+
AND status != 'backlog'
|
|
187
|
+
`).get() as { count: number } | undefined;
|
|
188
|
+
return (result?.count ?? 0) > 0;
|
|
189
|
+
} catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
174
194
|
// Seed onboarding epic into a blank project's database if not already present
|
|
175
195
|
function seedOnboardingIfBlank(db: Database.Database, dbPath: string): void {
|
|
176
196
|
try {
|
|
177
197
|
const projectPath = path.dirname(path.dirname(dbPath)); // .jettypod/work.db -> project root
|
|
178
198
|
if (!isBlankProject(projectPath)) return;
|
|
179
199
|
|
|
180
|
-
const existing = db.prepare("SELECT id FROM work_items WHERE type = 'epic' AND title = 'Project
|
|
200
|
+
const existing = db.prepare("SELECT id FROM work_items WHERE type = 'epic' AND title = 'Project Planning'").get();
|
|
181
201
|
if (existing) return;
|
|
182
202
|
|
|
183
203
|
const insert = db.prepare(
|
|
184
|
-
'INSERT INTO work_items (type, title, description, parent_id, status, created_at) VALUES (?, ?, ?, ?, ?, ?)'
|
|
204
|
+
'INSERT INTO work_items (type, title, description, parent_id, status, created_at, conversational) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
185
205
|
);
|
|
186
206
|
const now = new Date().toISOString();
|
|
187
207
|
|
|
188
|
-
const epicResult = insert.run('epic', 'Project
|
|
189
|
-
'Get your project set up and planned. Work through these chores one at a time — each one is a short conversation.', null, 'backlog', now);
|
|
208
|
+
const epicResult = insert.run('epic', 'Project Planning',
|
|
209
|
+
'Get your project set up and planned. Work through these chores one at a time — each one is a short conversation.', null, 'backlog', now, 0);
|
|
190
210
|
const epicId = epicResult.lastInsertRowid;
|
|
191
211
|
|
|
192
212
|
const chores = [
|
|
193
|
-
{ title: 'Align on the user journey', desc: 'Help the user define what their product does.\n\nCLAUDE SESSION GUIDANCE:\nOpen with: "What do users
|
|
213
|
+
{ title: 'Align on the user journey', desc: 'Help the user define what their product does.\n\nCLAUDE SESSION GUIDANCE:\nOpen with: "What do users do in this product?"\n\nOUTCOME:\n- A clear description of the core user journey' },
|
|
194
214
|
{ title: 'Explore UX approaches', desc: 'Help the user decide how the product should feel.\n\nCONTEXT FROM PREVIOUS CHORE:\nRead previous decisions first.\n\nCLAUDE SESSION GUIDANCE:\nPresent 3 UX approaches.\n\nOUTCOME:\n- 3 UX options compared\n- A winner chosen' },
|
|
195
215
|
{ title: 'Choose a tech stack', desc: 'Help the user pick the right tech stack.\n\nCONTEXT FROM PREVIOUS CHORES:\nRead previous decisions first.\n\nCLAUDE SESSION GUIDANCE:\nPresent 3 tech stack options.\n\nOUTCOME:\n- A tech stack chosen with rationale' },
|
|
196
216
|
{ title: 'Break the project into epics', desc: 'Break the project into buildable phases.\n\nCONTEXT FROM PREVIOUS CHORES:\nRead all previous decisions.\n\nCLAUDE SESSION GUIDANCE:\nPropose 3-5 epics.\n\nOUTCOME:\n- 3-5 epics created in the backlog' },
|
|
197
217
|
];
|
|
198
218
|
|
|
199
219
|
for (const chore of chores) {
|
|
200
|
-
insert.run('chore', chore.title, chore.desc, epicId, 'backlog', now);
|
|
220
|
+
insert.run('chore', chore.title, chore.desc, epicId, 'backlog', now, 1);
|
|
201
221
|
}
|
|
202
222
|
} catch (err) {
|
|
203
223
|
console.error('Could not seed onboarding:', err instanceof Error ? err.message : err);
|
|
@@ -283,7 +303,7 @@ export function getAllWorkItems(): WorkItem[] {
|
|
|
283
303
|
const db = getDb();
|
|
284
304
|
const items = db.prepare(`
|
|
285
305
|
SELECT id, type, title, description, status, parent_id, epic_id,
|
|
286
|
-
branch_name, mode, phase, completed_at, created_at
|
|
306
|
+
branch_name, mode, phase, completed_at, created_at, conversational
|
|
287
307
|
FROM work_items
|
|
288
308
|
ORDER BY id
|
|
289
309
|
`).all() as WorkItem[];
|
|
@@ -294,18 +314,33 @@ export function getWorkItem(id: number): WorkItem | null {
|
|
|
294
314
|
const db = getDb();
|
|
295
315
|
const item = db.prepare(`
|
|
296
316
|
SELECT id, type, title, description, status, parent_id, epic_id,
|
|
297
|
-
branch_name, mode, phase, completed_at, created_at
|
|
317
|
+
branch_name, mode, phase, completed_at, created_at, conversational,
|
|
318
|
+
ready_for_review, rejection_reason
|
|
298
319
|
FROM work_items
|
|
299
320
|
WHERE id = ?
|
|
300
321
|
`).get(id) as WorkItem | undefined;
|
|
301
322
|
return item || null;
|
|
302
323
|
}
|
|
303
324
|
|
|
325
|
+
export function createWorkItem(
|
|
326
|
+
type: 'epic' | 'feature' | 'chore' | 'bug',
|
|
327
|
+
title: string,
|
|
328
|
+
description?: string,
|
|
329
|
+
parentId?: number
|
|
330
|
+
): WorkItem {
|
|
331
|
+
const db = getWriteDb();
|
|
332
|
+
const now = new Date().toISOString();
|
|
333
|
+
const result = db.prepare(
|
|
334
|
+
'INSERT INTO work_items (type, title, description, parent_id, status, created_at, conversational) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
335
|
+
).run(type, title, description || null, parentId || null, 'backlog', now, 0);
|
|
336
|
+
return getWorkItem(result.lastInsertRowid as number)!;
|
|
337
|
+
}
|
|
338
|
+
|
|
304
339
|
export function getChildWorkItems(parentId: number): WorkItem[] {
|
|
305
340
|
const db = getDb();
|
|
306
341
|
const items = db.prepare(`
|
|
307
342
|
SELECT id, type, title, description, status, parent_id, epic_id,
|
|
308
|
-
branch_name, mode, phase, completed_at, created_at
|
|
343
|
+
branch_name, mode, phase, completed_at, created_at, conversational
|
|
309
344
|
FROM work_items
|
|
310
345
|
WHERE parent_id = ?
|
|
311
346
|
ORDER BY id
|
|
@@ -388,7 +423,7 @@ export function getActiveWork(): WorkItem[] {
|
|
|
388
423
|
const db = getDb();
|
|
389
424
|
const items = db.prepare(`
|
|
390
425
|
SELECT id, type, title, description, status, parent_id, epic_id,
|
|
391
|
-
branch_name, mode, phase, completed_at, created_at
|
|
426
|
+
branch_name, mode, phase, completed_at, created_at, conversational
|
|
392
427
|
FROM work_items
|
|
393
428
|
WHERE status = 'in_progress'
|
|
394
429
|
ORDER BY id
|
|
@@ -400,7 +435,7 @@ export function getRecentlyCompleted(limit: number = 10): WorkItem[] {
|
|
|
400
435
|
const db = getDb();
|
|
401
436
|
const items = db.prepare(`
|
|
402
437
|
SELECT id, type, title, description, status, parent_id, epic_id,
|
|
403
|
-
branch_name, mode, phase, completed_at, created_at
|
|
438
|
+
branch_name, mode, phase, completed_at, created_at, conversational
|
|
404
439
|
FROM work_items
|
|
405
440
|
WHERE status = 'done'
|
|
406
441
|
ORDER BY completed_at DESC
|
|
@@ -437,7 +472,7 @@ export function getKanbanData(doneLimit: number = 50): KanbanData {
|
|
|
437
472
|
// Get all chores that belong to features (for chore expansion)
|
|
438
473
|
const featureChores = db.prepare(`
|
|
439
474
|
SELECT c.id, c.type, c.title, c.description, c.status, c.parent_id, c.epic_id,
|
|
440
|
-
c.branch_name, c.mode, c.phase, c.completed_at, c.created_at,
|
|
475
|
+
c.branch_name, c.mode, c.phase, c.completed_at, c.created_at, c.conversational,
|
|
441
476
|
wc.current_step, wc.total_steps
|
|
442
477
|
FROM work_items c
|
|
443
478
|
INNER JOIN work_items f ON c.parent_id = f.id
|
|
@@ -459,7 +494,7 @@ export function getKanbanData(doneLimit: number = 50): KanbanData {
|
|
|
459
494
|
// Get all bugs that belong to features (for bug expansion)
|
|
460
495
|
const featureBugs = db.prepare(`
|
|
461
496
|
SELECT b.id, b.type, b.title, b.description, b.status, b.parent_id, b.epic_id,
|
|
462
|
-
b.branch_name, b.mode, b.phase, b.completed_at, b.created_at
|
|
497
|
+
b.branch_name, b.mode, b.phase, b.completed_at, b.created_at, b.conversational
|
|
463
498
|
FROM work_items b
|
|
464
499
|
INNER JOIN work_items f ON b.parent_id = f.id
|
|
465
500
|
WHERE b.type = 'bug' AND f.type = 'feature'
|
|
@@ -482,7 +517,8 @@ export function getKanbanData(doneLimit: number = 50): KanbanData {
|
|
|
482
517
|
// - Bugs if they exist
|
|
483
518
|
const allItems = db.prepare(`
|
|
484
519
|
SELECT w.id, w.type, w.title, w.description, w.status, w.parent_id, w.epic_id,
|
|
485
|
-
w.branch_name, w.mode, w.phase, w.completed_at, w.created_at, w.display_order,
|
|
520
|
+
w.branch_name, w.mode, w.phase, w.completed_at, w.created_at, w.conversational, w.display_order,
|
|
521
|
+
w.ready_for_review,
|
|
486
522
|
p.type as parent_type,
|
|
487
523
|
wc.current_step, wc.total_steps
|
|
488
524
|
FROM work_items w
|
|
@@ -547,6 +583,13 @@ export function getKanbanData(doneLimit: number = 50): KanbanData {
|
|
|
547
583
|
}
|
|
548
584
|
}
|
|
549
585
|
|
|
586
|
+
// Sort in-flight: review items first (ascending by id), then non-review (ascending by id)
|
|
587
|
+
inFlight.sort((a, b) => {
|
|
588
|
+
if (a.ready_for_review && !b.ready_for_review) return -1;
|
|
589
|
+
if (!a.ready_for_review && b.ready_for_review) return 1;
|
|
590
|
+
return a.id - b.id;
|
|
591
|
+
});
|
|
592
|
+
|
|
550
593
|
// Always ensure an ungrouped entry exists in backlog for drag-drop target
|
|
551
594
|
if (!backlogGroups.has('ungrouped')) {
|
|
552
595
|
backlogGroups.set('ungrouped', {
|
|
@@ -610,17 +653,42 @@ export function updateWorkItemDescription(id: number, description: string): bool
|
|
|
610
653
|
|
|
611
654
|
export function updateWorkItemStatus(id: number, status: string, rejectionReason?: string): boolean {
|
|
612
655
|
const db = getWriteDb();
|
|
656
|
+
// Normalize rejection reason: treat empty/whitespace-only as no rejection
|
|
657
|
+
const normalizedRejection = rejectionReason?.trim() || undefined;
|
|
613
658
|
const completedAt = status === 'done' ? new Date().toISOString() : null;
|
|
614
|
-
const rejectedAt =
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
659
|
+
const rejectedAt = normalizedRejection ? new Date().toISOString() : null;
|
|
660
|
+
// Clear ready_for_review on accept or reject
|
|
661
|
+
const readyForReview = (status === 'done' || normalizedRejection) ? 0 : undefined;
|
|
662
|
+
const result = readyForReview !== undefined
|
|
663
|
+
? db.prepare(`
|
|
664
|
+
UPDATE work_items SET status = ?, completed_at = ?, rejection_reason = ?, rejected_at = ?, ready_for_review = ? WHERE id = ?
|
|
665
|
+
`).run(status, completedAt, normalizedRejection ?? null, rejectedAt, readyForReview, id)
|
|
666
|
+
: db.prepare(`
|
|
667
|
+
UPDATE work_items SET status = ?, completed_at = ?, rejection_reason = ?, rejected_at = ? WHERE id = ?
|
|
668
|
+
`).run(status, completedAt, normalizedRejection ?? null, rejectedAt, id);
|
|
618
669
|
|
|
619
670
|
// When work item is cancelled, mark any linked sessions as orphaned
|
|
620
671
|
if (status === 'cancelled' && result.changes > 0) {
|
|
621
672
|
orphanSessionsByWorkItem(id);
|
|
622
673
|
}
|
|
623
674
|
|
|
675
|
+
// When a chore is marked done, check if all sibling chores are done
|
|
676
|
+
// and set ready_for_review on the parent feature.
|
|
677
|
+
// Edge cases: no chores (length 0) → no auto-set; cancelled chores ≠ done → no auto-set
|
|
678
|
+
if (status === 'done' && result.changes > 0) {
|
|
679
|
+
const item = db.prepare('SELECT parent_id, type FROM work_items WHERE id = ?').get(id) as { parent_id: number | null; type: string } | undefined;
|
|
680
|
+
if (item?.parent_id && item.type === 'chore') {
|
|
681
|
+
const parent = db.prepare('SELECT id, type FROM work_items WHERE id = ?').get(item.parent_id) as { id: number; type: string } | undefined;
|
|
682
|
+
if (parent?.type === 'feature') {
|
|
683
|
+
const siblings = db.prepare('SELECT status FROM work_items WHERE parent_id = ? AND type = ?').all(parent.id, 'chore') as { status: string }[];
|
|
684
|
+
const allDone = siblings.length > 0 && siblings.every(s => s.status === 'done');
|
|
685
|
+
if (allDone) {
|
|
686
|
+
db.prepare('UPDATE work_items SET ready_for_review = 1 WHERE id = ?').run(parent.id);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
624
692
|
return result.changes > 0;
|
|
625
693
|
}
|
|
626
694
|
|
|
@@ -1224,3 +1292,51 @@ export function setMainBranch(branch: string | null): void {
|
|
|
1224
1292
|
}
|
|
1225
1293
|
writeConfig(config);
|
|
1226
1294
|
}
|
|
1295
|
+
|
|
1296
|
+
// ==================== Usage Tracking ====================
|
|
1297
|
+
|
|
1298
|
+
const FREE_WEEKLY_LIMIT = 20;
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Get the Monday 00:00:00 UTC of the current week.
|
|
1302
|
+
* Returns format matching SQLite's CURRENT_TIMESTAMP ("YYYY-MM-DD HH:MM:SS")
|
|
1303
|
+
* so lexicographic comparison in WHERE clauses works correctly.
|
|
1304
|
+
*/
|
|
1305
|
+
function getCurrentWeekStart(): string {
|
|
1306
|
+
const now = new Date();
|
|
1307
|
+
const day = now.getUTCDay(); // 0=Sun, 1=Mon, ...
|
|
1308
|
+
const diff = day === 0 ? 6 : day - 1; // days since Monday
|
|
1309
|
+
const monday = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - diff));
|
|
1310
|
+
return monday.toISOString().replace('T', ' ').replace('.000Z', '');
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
export interface WeeklyUsage {
|
|
1314
|
+
used: number;
|
|
1315
|
+
limit: number;
|
|
1316
|
+
remaining: number;
|
|
1317
|
+
allowed: boolean;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Count work items created on the free plan during the current week.
|
|
1322
|
+
*/
|
|
1323
|
+
export function getWeeklyUsage(): WeeklyUsage {
|
|
1324
|
+
const db = getDb();
|
|
1325
|
+
const weekStart = getCurrentWeekStart();
|
|
1326
|
+
const projectRoot = getProjectRoot();
|
|
1327
|
+
|
|
1328
|
+
console.log('[usage] getWeeklyUsage called', { projectRoot, weekStart });
|
|
1329
|
+
|
|
1330
|
+
const row = db.prepare(
|
|
1331
|
+
`SELECT COUNT(*) as count FROM work_items
|
|
1332
|
+
WHERE plan_at_creation = 'free' AND created_at >= ?`
|
|
1333
|
+
).get(weekStart) as { count: number } | undefined;
|
|
1334
|
+
|
|
1335
|
+
const used = row?.count ?? 0;
|
|
1336
|
+
const remaining = Math.max(0, FREE_WEEKLY_LIMIT - used);
|
|
1337
|
+
const result = { used, limit: FREE_WEEKLY_LIMIT, remaining, allowed: remaining > 0 };
|
|
1338
|
+
|
|
1339
|
+
console.log('[usage] getWeeklyUsage result', { row, result });
|
|
1340
|
+
|
|
1341
|
+
return result;
|
|
1342
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { InFlightItem, KanbanGroup, WorkItem } from '@/lib/db';
|
|
2
|
+
|
|
3
|
+
export interface KanbanData {
|
|
4
|
+
inFlight: InFlightItem[];
|
|
5
|
+
backlog: Map<string, KanbanGroup>;
|
|
6
|
+
done: Map<string, KanbanGroup>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Helper to find item by ID in the kanban data
|
|
10
|
+
export function findItemById(data: KanbanData, id: number): { item: InFlightItem; status: string } | null {
|
|
11
|
+
const inFlightItem = data.inFlight.find(item => item.id === id);
|
|
12
|
+
if (inFlightItem) return { item: inFlightItem, status: 'in_progress' };
|
|
13
|
+
|
|
14
|
+
for (const group of data.backlog.values()) {
|
|
15
|
+
const backlogItem = group.items.find(item => item.id === id);
|
|
16
|
+
if (backlogItem) return { item: backlogItem as InFlightItem, status: 'backlog' };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const group of data.done.values()) {
|
|
20
|
+
const doneItem = group.items.find(item => item.id === id);
|
|
21
|
+
if (doneItem) return { item: doneItem as InFlightItem, status: 'done' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Extract onboarding chore items from kanban data for the OnboardingWelcome component
|
|
28
|
+
export function getOnboardingItems(data: KanbanData): WorkItem[] {
|
|
29
|
+
for (const group of data.backlog.values()) {
|
|
30
|
+
if (group.epicTitle === 'Project Planning') {
|
|
31
|
+
return group.items;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Build a status map from kanban data (item id -> status string)
|
|
38
|
+
export function buildStatusMap(kanbanData: KanbanData): Map<number, string> {
|
|
39
|
+
const map = new Map<number, string>();
|
|
40
|
+
for (const item of kanbanData.inFlight) {
|
|
41
|
+
map.set(item.id, item.status);
|
|
42
|
+
}
|
|
43
|
+
for (const group of kanbanData.backlog.values()) {
|
|
44
|
+
for (const item of group.items) {
|
|
45
|
+
map.set(item.id, item.status);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const group of kanbanData.done.values()) {
|
|
49
|
+
for (const item of group.items) {
|
|
50
|
+
map.set(item.id, item.status);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return map;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Build a mode map from kanban data (feature id -> mode) for detecting mode transitions
|
|
57
|
+
export function buildModeMap(kanbanData: KanbanData): Map<number, string | null> {
|
|
58
|
+
const map = new Map<number, string | null>();
|
|
59
|
+
const collectFeatures = (items: WorkItem[]) => {
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
if (item.type === 'feature') {
|
|
62
|
+
map.set(item.id, item.mode);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
collectFeatures(kanbanData.inFlight);
|
|
67
|
+
for (const group of kanbanData.backlog.values()) collectFeatures(group.items);
|
|
68
|
+
for (const group of kanbanData.done.values()) collectFeatures(group.items);
|
|
69
|
+
return map;
|
|
70
|
+
}
|
|
@@ -99,14 +99,36 @@ function runMigrations(betterDb) {
|
|
|
99
99
|
)
|
|
100
100
|
`);
|
|
101
101
|
|
|
102
|
-
//
|
|
103
|
-
|
|
102
|
+
// Create _meta table for schema versioning
|
|
103
|
+
betterDb.exec(`
|
|
104
|
+
CREATE TABLE IF NOT EXISTS _meta (
|
|
105
|
+
key TEXT PRIMARY KEY,
|
|
106
|
+
value TEXT
|
|
107
|
+
)
|
|
108
|
+
`);
|
|
104
109
|
|
|
105
110
|
// Load migration files
|
|
106
111
|
const files = fs.readdirSync(migrationsDir)
|
|
107
112
|
.filter(f => f.endsWith('.js') && f !== 'index.js' && !f.endsWith('.test.js'))
|
|
108
113
|
.sort();
|
|
109
114
|
|
|
115
|
+
const knownMigrationCount = files.length;
|
|
116
|
+
|
|
117
|
+
// Check schema version for forward-compatibility
|
|
118
|
+
const versionRow = betterDb.prepare('SELECT value FROM _meta WHERE key = ?').get('schema_version');
|
|
119
|
+
const currentVersion = versionRow ? parseInt(versionRow.value, 10) : 0;
|
|
120
|
+
|
|
121
|
+
if (currentVersion > knownMigrationCount) {
|
|
122
|
+
console.warn(
|
|
123
|
+
`⚠️ Database schema version (${currentVersion}) is newer than this version of JettyPod supports (${knownMigrationCount}).` +
|
|
124
|
+
'\n Please update JettyPod to avoid compatibility issues.'
|
|
125
|
+
);
|
|
126
|
+
return; // Don't run migrations — the DB is from a newer version
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Get already-applied migrations
|
|
130
|
+
const applied = betterDb.prepare('SELECT id FROM migrations').all().map(r => r.id);
|
|
131
|
+
|
|
110
132
|
// Create shim for callback-based migration API
|
|
111
133
|
const shim = createDbShim(betterDb);
|
|
112
134
|
|
|
@@ -140,6 +162,9 @@ function runMigrations(betterDb) {
|
|
|
140
162
|
throw err;
|
|
141
163
|
}
|
|
142
164
|
}
|
|
165
|
+
|
|
166
|
+
// Update schema version to reflect current state
|
|
167
|
+
betterDb.prepare('INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)').run('schema_version', String(knownMigrationCount));
|
|
143
168
|
}
|
|
144
169
|
|
|
145
170
|
module.exports = { runMigrations };
|
|
@@ -43,6 +43,7 @@ export type SessionState = 'idle' | 'connecting' | 'streaming' | 'done' | 'error
|
|
|
43
43
|
/** All possible state transition events */
|
|
44
44
|
export type SessionEvent =
|
|
45
45
|
| 'SEND' // User sends a message
|
|
46
|
+
| 'QUEUE' // User sends a message while streaming — queued for later
|
|
46
47
|
| 'CONNECTED' // Connection established, streaming begins
|
|
47
48
|
| 'COMPLETE' // Stream completed successfully
|
|
48
49
|
| 'ERROR' // An error occurred
|
|
@@ -82,11 +83,13 @@ const TRANSITIONS: Record<SessionState, Partial<Record<SessionEvent, SessionStat
|
|
|
82
83
|
CLEAR: 'idle',
|
|
83
84
|
},
|
|
84
85
|
connecting: {
|
|
86
|
+
QUEUE: 'connecting', // Queue message while connecting (stays in same state)
|
|
85
87
|
CONNECTED: 'streaming',
|
|
86
88
|
ERROR: 'error',
|
|
87
89
|
STOP: 'idle',
|
|
88
90
|
},
|
|
89
91
|
streaming: {
|
|
92
|
+
QUEUE: 'streaming', // Queue message while streaming (stays in same state)
|
|
90
93
|
COMPLETE: 'done',
|
|
91
94
|
ERROR: 'error',
|
|
92
95
|
STOP: 'idle',
|