jettypod 4.4.115 → 4.4.118
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 +25 -9
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +7 -3
- 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/connect-claude/page.tsx +24 -0
- package/apps/dashboard/app/install-claude/page.tsx +8 -6
- package/apps/dashboard/app/login/page.tsx +229 -0
- package/apps/dashboard/app/page.tsx +5 -3
- package/apps/dashboard/app/settings/page.tsx +2 -0
- package/apps/dashboard/app/subscribe/page.tsx +11 -0
- package/apps/dashboard/app/welcome/page.tsx +23 -0
- package/apps/dashboard/components/AppShell.tsx +51 -9
- package/apps/dashboard/components/CardMenu.tsx +14 -5
- package/apps/dashboard/components/ClaudePanel.tsx +65 -9
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +223 -0
- package/apps/dashboard/components/DragContext.tsx +73 -64
- package/apps/dashboard/components/DraggableCard.tsx +6 -46
- package/apps/dashboard/components/GateCard.tsx +21 -0
- package/apps/dashboard/components/InstallClaudeScreen.tsx +132 -30
- package/apps/dashboard/components/KanbanBoard.tsx +173 -56
- package/apps/dashboard/components/PlaceholderCard.tsx +9 -19
- package/apps/dashboard/components/ProjectSwitcher.tsx +28 -0
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +34 -3
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +30 -2
- package/apps/dashboard/components/SubscribeContent.tsx +191 -0
- package/apps/dashboard/components/TipCard.tsx +176 -0
- package/apps/dashboard/components/UpgradeBanner.tsx +29 -0
- package/apps/dashboard/components/WelcomeScreen.tsx +14 -4
- package/apps/dashboard/components/settings/AccountSection.tsx +163 -0
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +292 -29
- package/apps/dashboard/contexts/UsageContext.tsx +131 -0
- package/apps/dashboard/contexts/usageHelpers.js +9 -0
- package/apps/dashboard/electron/ipc-handlers.js +220 -114
- package/apps/dashboard/electron/main.js +415 -37
- package/apps/dashboard/electron/preload.js +23 -4
- package/apps/dashboard/electron/session-manager.js +141 -0
- package/apps/dashboard/electron-builder.config.js +3 -5
- package/apps/dashboard/lib/claude-process-manager.ts +6 -4
- package/apps/dashboard/lib/db-bridge.ts +32 -0
- package/apps/dashboard/lib/db.ts +159 -13
- package/apps/dashboard/lib/session-state-machine.ts +3 -0
- package/apps/dashboard/lib/session-stream-manager.ts +76 -13
- package/apps/dashboard/lib/tests.ts +3 -1
- package/apps/dashboard/next.config.js +19 -14
- package/apps/dashboard/package.json +3 -1
- package/apps/dashboard/scripts/upload-to-r2.js +89 -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 +1074 -0
- package/apps/update-server/tsconfig.json +16 -0
- package/apps/update-server/wrangler.toml +35 -0
- package/docs/bdd-guidance.md +390 -0
- package/jettypod.js +5 -4
- package/lib/migrations/027-plan-at-creation-column.js +31 -0
- package/lib/migrations/028-ready-for-review-column.js +27 -0
- package/lib/schema.js +3 -1
- package/lib/seed-onboarding.js +100 -68
- package/lib/work-commands/index.js +43 -13
- package/lib/work-tracking/index.js +46 -27
- package/package.json +1 -1
- package/skills-templates/bug-mode/SKILL.md +5 -11
- package/skills-templates/request-routing/SKILL.md +24 -11
- package/skills-templates/simple-improvement/SKILL.md +35 -19
- package/skills-templates/stable-mode/SKILL.md +5 -6
- 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
|
@@ -29,6 +29,12 @@ export type StreamStatus = 'idle' | 'connecting' | 'creating' | 'streaming' | 'd
|
|
|
29
29
|
export interface SessionContext {
|
|
30
30
|
workItemId: string;
|
|
31
31
|
standalone: boolean;
|
|
32
|
+
conversational?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface QueuedMessage {
|
|
36
|
+
message: string;
|
|
37
|
+
images?: AttachedImage[];
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
export interface StreamState {
|
|
@@ -40,6 +46,7 @@ export interface StreamState {
|
|
|
40
46
|
isReconnecting: boolean;
|
|
41
47
|
reconnectAttempt: number;
|
|
42
48
|
narratedMode: boolean;
|
|
49
|
+
queuedMessage: QueuedMessage | null;
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
export interface StreamManagerCallbacks {
|
|
@@ -267,8 +274,12 @@ export class SessionStreamManager {
|
|
|
267
274
|
private _reconnectAttempt: number = 0;
|
|
268
275
|
private _narratedMode: boolean = true;
|
|
269
276
|
private _pendingQuestion: ClaudeMessage | null = null;
|
|
277
|
+
private _queuedMessage: QueuedMessage | null = null;
|
|
270
278
|
private _isFirstMessage: boolean = true;
|
|
271
279
|
|
|
280
|
+
// Notification batching — coalesces rapid state changes into one callback per frame
|
|
281
|
+
private _notifyPending: boolean = false;
|
|
282
|
+
|
|
272
283
|
// Control
|
|
273
284
|
private abortController: AbortController | null = null;
|
|
274
285
|
private creatingStatusTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -327,6 +338,27 @@ export class SessionStreamManager {
|
|
|
327
338
|
return this._pendingQuestion;
|
|
328
339
|
}
|
|
329
340
|
|
|
341
|
+
get queuedMessage(): QueuedMessage | null {
|
|
342
|
+
return this._queuedMessage;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Queue a message to be sent after the current stream completes.
|
|
347
|
+
* Only one message can be queued at a time (last one wins).
|
|
348
|
+
*/
|
|
349
|
+
queueMessage(message: string, images?: AttachedImage[]): void {
|
|
350
|
+
this._queuedMessage = { message, images };
|
|
351
|
+
this.notifyStateChange();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Clear a queued message (e.g., if user stops the stream).
|
|
356
|
+
*/
|
|
357
|
+
clearQueuedMessage(): void {
|
|
358
|
+
this._queuedMessage = null;
|
|
359
|
+
this.notifyStateChange();
|
|
360
|
+
}
|
|
361
|
+
|
|
330
362
|
setNarratedMode(enabled: boolean): void {
|
|
331
363
|
this._narratedMode = enabled;
|
|
332
364
|
this.notifyStateChange();
|
|
@@ -351,6 +383,7 @@ export class SessionStreamManager {
|
|
|
351
383
|
isReconnecting: this._isReconnecting,
|
|
352
384
|
reconnectAttempt: this._reconnectAttempt,
|
|
353
385
|
narratedMode: this._narratedMode,
|
|
386
|
+
queuedMessage: this._queuedMessage,
|
|
354
387
|
};
|
|
355
388
|
}
|
|
356
389
|
|
|
@@ -359,7 +392,20 @@ export class SessionStreamManager {
|
|
|
359
392
|
// -------------------------------------------------------------------------
|
|
360
393
|
|
|
361
394
|
private notifyStateChange(): void {
|
|
362
|
-
|
|
395
|
+
if (!this._notifyPending) {
|
|
396
|
+
this._notifyPending = true;
|
|
397
|
+
const notify = () => {
|
|
398
|
+
this._notifyPending = false;
|
|
399
|
+
this.callbacks.onStateChange?.(this.getState());
|
|
400
|
+
};
|
|
401
|
+
// Batch rapid state changes (e.g., multiple stream events per frame) into one callback.
|
|
402
|
+
// requestAnimationFrame syncs with display refresh (~16ms); queueMicrotask as fallback.
|
|
403
|
+
if (typeof requestAnimationFrame !== 'undefined') {
|
|
404
|
+
requestAnimationFrame(notify);
|
|
405
|
+
} else {
|
|
406
|
+
queueMicrotask(notify);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
363
409
|
}
|
|
364
410
|
|
|
365
411
|
setMessages(messages: ClaudeMessage[]): void {
|
|
@@ -401,6 +447,11 @@ export class SessionStreamManager {
|
|
|
401
447
|
// Check for gate markers in tool results and text content
|
|
402
448
|
const gate = extractGateFromMessage(message);
|
|
403
449
|
if (gate) {
|
|
450
|
+
// Skip if the previous gate has the same gateType (prevents duplicate consecutive cards)
|
|
451
|
+
const lastGate = [...this._messages].reverse().find(m => m.type === 'gate');
|
|
452
|
+
if (lastGate && lastGate.gateType === gate.gateType) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
404
455
|
// Add the gate message instead of (or in addition to) the raw message
|
|
405
456
|
this._messages = [...this._messages, gate];
|
|
406
457
|
this._narratedMode = true; // Auto-enable narrated mode on first gate
|
|
@@ -508,19 +559,25 @@ export class SessionStreamManager {
|
|
|
508
559
|
};
|
|
509
560
|
this.addMessage(userMessage);
|
|
510
561
|
|
|
511
|
-
// For first message in a new session, show "creating" status
|
|
562
|
+
// For first message in a new session, show "creating" status
|
|
563
|
+
// Conversational sessions skip the 5s delay and go straight to streaming
|
|
512
564
|
if (this._isFirstMessage) {
|
|
513
565
|
this._isFirstMessage = false;
|
|
514
|
-
this.
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
566
|
+
if (this.sessionContext.conversational) {
|
|
567
|
+
this._status = 'streaming';
|
|
568
|
+
this.notifyStateChange();
|
|
569
|
+
} else {
|
|
570
|
+
this._status = 'creating';
|
|
571
|
+
this.notifyStateChange();
|
|
572
|
+
|
|
573
|
+
// Transition to streaming after 5 seconds
|
|
574
|
+
this.creatingStatusTimeout = setTimeout(() => {
|
|
575
|
+
if (this._status === 'creating') {
|
|
576
|
+
this._status = 'streaming';
|
|
577
|
+
this.notifyStateChange();
|
|
578
|
+
}
|
|
579
|
+
}, 5000);
|
|
580
|
+
}
|
|
524
581
|
} else {
|
|
525
582
|
// Set status to streaming
|
|
526
583
|
this._status = 'streaming';
|
|
@@ -541,7 +598,6 @@ export class SessionStreamManager {
|
|
|
541
598
|
},
|
|
542
599
|
body: JSON.stringify({
|
|
543
600
|
message,
|
|
544
|
-
conversationHistory: this._messages.slice(0, -1), // Exclude the user message we just added
|
|
545
601
|
images: images?.map(img => ({
|
|
546
602
|
type: img.type,
|
|
547
603
|
data: img.dataUrl,
|
|
@@ -667,6 +723,7 @@ export class SessionStreamManager {
|
|
|
667
723
|
this._status = 'idle';
|
|
668
724
|
this._isReconnecting = false;
|
|
669
725
|
this._reconnectAttempt = 0;
|
|
726
|
+
this._queuedMessage = null; // Clear queue on stop
|
|
670
727
|
this.notifyStateChange();
|
|
671
728
|
}
|
|
672
729
|
|
|
@@ -681,6 +738,7 @@ export class SessionStreamManager {
|
|
|
681
738
|
this._canRetry = false;
|
|
682
739
|
this._isReconnecting = false;
|
|
683
740
|
this._reconnectAttempt = 0;
|
|
741
|
+
this._queuedMessage = null;
|
|
684
742
|
this.lastMessage = null;
|
|
685
743
|
this.lastImages = undefined;
|
|
686
744
|
this.notifyStateChange();
|
|
@@ -717,6 +775,11 @@ export class SessionStreamManager {
|
|
|
717
775
|
* This adds the gate directly to the message list without parsing from stream text.
|
|
718
776
|
*/
|
|
719
777
|
injectGate(gateType: string, gateData: Record<string, unknown> = {}): void {
|
|
778
|
+
// Skip if the previous gate has the same type (prevents duplicate consecutive cards)
|
|
779
|
+
const lastGate = [...this._messages].reverse().find(m => m.type === 'gate');
|
|
780
|
+
if (lastGate && lastGate.gateType === gateType) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
720
783
|
const gate: ClaudeMessage = {
|
|
721
784
|
type: 'gate',
|
|
722
785
|
gateType,
|
|
@@ -12,6 +12,7 @@ export interface TestScenario {
|
|
|
12
12
|
title: string;
|
|
13
13
|
status: 'pass' | 'fail' | 'pending';
|
|
14
14
|
duration: string;
|
|
15
|
+
lastRun: string | null;
|
|
15
16
|
error?: string;
|
|
16
17
|
failedStep?: string;
|
|
17
18
|
steps: string[];
|
|
@@ -200,7 +201,7 @@ export function getTestDashboardData(): TestDashboardData {
|
|
|
200
201
|
const featureFiles = getFeatureFiles(projectRoot);
|
|
201
202
|
|
|
202
203
|
// Build a map of scenario name → latest DB result
|
|
203
|
-
let dbResults: Map<string, { status: string; duration_ms: number; error_message: string | null; failed_step: string | null }>;
|
|
204
|
+
let dbResults: Map<string, { status: string; duration_ms: number; error_message: string | null; failed_step: string | null; run_at: string }>;
|
|
204
205
|
let lastRun: string | null = null;
|
|
205
206
|
try {
|
|
206
207
|
const rows = getLatestResults();
|
|
@@ -248,6 +249,7 @@ export function getTestDashboardData(): TestDashboardData {
|
|
|
248
249
|
title: scenario.name,
|
|
249
250
|
status,
|
|
250
251
|
duration,
|
|
252
|
+
lastRun: result?.run_at || null,
|
|
251
253
|
error,
|
|
252
254
|
failedStep,
|
|
253
255
|
steps: scenario.steps,
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
|
|
3
|
-
// Dashboard is at apps/dashboard, jettypod lib is at ../../lib relative to that
|
|
4
|
-
// process.cwd() is the dashboard directory during build
|
|
5
|
-
const jettypodLibPath = path.resolve(process.cwd(), '../../lib');
|
|
6
|
-
|
|
7
3
|
/** @type {import('next').NextConfig} */
|
|
8
4
|
const nextConfig = {
|
|
9
5
|
// Externalize modules with native bindings or dynamic requires
|
|
@@ -19,21 +15,30 @@ const nextConfig = {
|
|
|
19
15
|
|
|
20
16
|
webpack: (config, { isServer }) => {
|
|
21
17
|
if (isServer) {
|
|
22
|
-
// Externalize the jettypod lib using absolute path resolution
|
|
23
18
|
config.externals = config.externals || [];
|
|
24
|
-
config.externals.push(({ request
|
|
25
|
-
// Externalize
|
|
19
|
+
config.externals.push(({ request }, callback) => {
|
|
20
|
+
// Externalize worktree-facade with RUNTIME path resolution.
|
|
21
|
+
// Uses 'var' external type so the path expression evaluates at runtime,
|
|
22
|
+
// not at build time (which would bake in the build machine's absolute path).
|
|
23
|
+
// Packaged app: JETTYPOD_RESOURCES_PATH/bin/lib/<module>
|
|
24
|
+
// Dev mode: process.cwd()/../../lib/<module>
|
|
26
25
|
if (request && request.includes('lib/worktree-facade')) {
|
|
27
|
-
// Use absolute path to jettypod lib
|
|
28
26
|
const moduleName = request.split('lib/')[1];
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
return callback(null,
|
|
28
|
+
`var require(process.env.JETTYPOD_RESOURCES_PATH ` +
|
|
29
|
+
`? require('path').join(process.env.JETTYPOD_RESOURCES_PATH, 'bin', 'lib', '${moduleName}') ` +
|
|
30
|
+
`: require('path').resolve(process.cwd(), '../../lib', '${moduleName}'))`
|
|
31
|
+
);
|
|
31
32
|
}
|
|
32
|
-
// Externalize run-migrations.js
|
|
33
|
-
//
|
|
33
|
+
// Externalize run-migrations.js with RUNTIME path resolution.
|
|
34
|
+
// It uses dynamic require() to load migration files, which webpack
|
|
35
|
+
// replaces with a dead stub if bundled.
|
|
36
|
+
// In both packaged and dev: process.cwd() is the dashboard dir,
|
|
37
|
+
// and run-migrations.js is at lib/run-migrations.js relative to it.
|
|
34
38
|
if (request && request.includes('run-migrations')) {
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
return callback(null,
|
|
40
|
+
`var require(require('path').join(process.cwd(), 'lib', 'run-migrations'))`
|
|
41
|
+
);
|
|
37
42
|
}
|
|
38
43
|
callback(undefined);
|
|
39
44
|
});
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"electron:build:mac:universal": "npm run build && npx electron-builder --config electron-builder.config.js --mac --universal",
|
|
19
19
|
"electron:build:win": "npm run build && npx electron-builder --config electron-builder.config.js --win",
|
|
20
20
|
"electron:build:linux": "npm run build && npx electron-builder --config electron-builder.config.js --linux",
|
|
21
|
-
"electron:pack": "npm run build && npx electron-builder --config electron-builder.config.js --dir"
|
|
21
|
+
"electron:pack": "npm run build && npx electron-builder --config electron-builder.config.js --dir",
|
|
22
|
+
"upload:r2": "node scripts/upload-to-r2.js",
|
|
23
|
+
"electron:release": "npm run electron:build:mac && npm run upload:r2"
|
|
22
24
|
},
|
|
23
25
|
"dependencies": {
|
|
24
26
|
"@dnd-kit/core": "^6.3.1",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Upload release artifacts to Cloudflare R2.
|
|
5
|
+
* Run after electron-builder: npm run upload:r2
|
|
6
|
+
*
|
|
7
|
+
* Requires CLOUDFLARE_API_TOKEN env var (or wrangler login).
|
|
8
|
+
* Uploads: latest-mac.yml, DMG, ZIP, and blockmap files.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const BUCKET_NAME = 'jettypod-releases';
|
|
16
|
+
const DIST_DIR = path.join(__dirname, '..', 'dist');
|
|
17
|
+
|
|
18
|
+
// File patterns to upload
|
|
19
|
+
const UPLOAD_PATTERNS = [
|
|
20
|
+
/^latest-mac\.yml$/,
|
|
21
|
+
/\.dmg$/,
|
|
22
|
+
/\.zip$/,
|
|
23
|
+
/\.blockmap$/,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function findArtifacts() {
|
|
27
|
+
if (!fs.existsSync(DIST_DIR)) {
|
|
28
|
+
console.error(`❌ dist/ directory not found at ${DIST_DIR}`);
|
|
29
|
+
console.error('Run electron:build first.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const files = fs.readdirSync(DIST_DIR);
|
|
34
|
+
return files.filter((file) =>
|
|
35
|
+
UPLOAD_PATTERNS.some((pattern) => pattern.test(file))
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function uploadFile(filename) {
|
|
40
|
+
const filePath = path.join(DIST_DIR, filename);
|
|
41
|
+
const stats = fs.statSync(filePath);
|
|
42
|
+
const sizeMB = (stats.size / (1024 * 1024)).toFixed(1);
|
|
43
|
+
|
|
44
|
+
console.log(` Uploading ${filename} (${sizeMB} MB)...`);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
execSync(
|
|
48
|
+
`npx wrangler r2 object put "${BUCKET_NAME}/${filename}" --file="${filePath}" --remote`,
|
|
49
|
+
{ stdio: 'pipe' }
|
|
50
|
+
);
|
|
51
|
+
console.log(` ✅ ${filename}`);
|
|
52
|
+
return true;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(` ❌ Failed to upload ${filename}: ${error.message}`);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function main() {
|
|
60
|
+
console.log('🚀 Uploading release artifacts to R2...\n');
|
|
61
|
+
|
|
62
|
+
const artifacts = findArtifacts();
|
|
63
|
+
if (artifacts.length === 0) {
|
|
64
|
+
console.error('❌ No release artifacts found in dist/');
|
|
65
|
+
console.error('Expected: latest-mac.yml, .dmg, .zip, or .blockmap files');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`Found ${artifacts.length} artifact(s):\n`);
|
|
70
|
+
|
|
71
|
+
let success = 0;
|
|
72
|
+
let failed = 0;
|
|
73
|
+
|
|
74
|
+
for (const artifact of artifacts) {
|
|
75
|
+
if (uploadFile(artifact)) {
|
|
76
|
+
success++;
|
|
77
|
+
} else {
|
|
78
|
+
failed++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(`\n📦 Upload complete: ${success} uploaded, ${failed} failed`);
|
|
83
|
+
|
|
84
|
+
if (failed > 0) {
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
main();
|