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.
Files changed (73) hide show
  1. package/.env +7 -0
  2. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +25 -9
  3. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +7 -3
  4. package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
  5. package/apps/dashboard/app/api/usage/route.ts +17 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +24 -0
  7. package/apps/dashboard/app/install-claude/page.tsx +8 -6
  8. package/apps/dashboard/app/login/page.tsx +229 -0
  9. package/apps/dashboard/app/page.tsx +5 -3
  10. package/apps/dashboard/app/settings/page.tsx +2 -0
  11. package/apps/dashboard/app/subscribe/page.tsx +11 -0
  12. package/apps/dashboard/app/welcome/page.tsx +23 -0
  13. package/apps/dashboard/components/AppShell.tsx +51 -9
  14. package/apps/dashboard/components/CardMenu.tsx +14 -5
  15. package/apps/dashboard/components/ClaudePanel.tsx +65 -9
  16. package/apps/dashboard/components/ConnectClaudeScreen.tsx +223 -0
  17. package/apps/dashboard/components/DragContext.tsx +73 -64
  18. package/apps/dashboard/components/DraggableCard.tsx +6 -46
  19. package/apps/dashboard/components/GateCard.tsx +21 -0
  20. package/apps/dashboard/components/InstallClaudeScreen.tsx +132 -30
  21. package/apps/dashboard/components/KanbanBoard.tsx +173 -56
  22. package/apps/dashboard/components/PlaceholderCard.tsx +9 -19
  23. package/apps/dashboard/components/ProjectSwitcher.tsx +28 -0
  24. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +34 -3
  25. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +30 -2
  26. package/apps/dashboard/components/SubscribeContent.tsx +191 -0
  27. package/apps/dashboard/components/TipCard.tsx +176 -0
  28. package/apps/dashboard/components/UpgradeBanner.tsx +29 -0
  29. package/apps/dashboard/components/WelcomeScreen.tsx +14 -4
  30. package/apps/dashboard/components/settings/AccountSection.tsx +163 -0
  31. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +292 -29
  32. package/apps/dashboard/contexts/UsageContext.tsx +131 -0
  33. package/apps/dashboard/contexts/usageHelpers.js +9 -0
  34. package/apps/dashboard/electron/ipc-handlers.js +220 -114
  35. package/apps/dashboard/electron/main.js +415 -37
  36. package/apps/dashboard/electron/preload.js +23 -4
  37. package/apps/dashboard/electron/session-manager.js +141 -0
  38. package/apps/dashboard/electron-builder.config.js +3 -5
  39. package/apps/dashboard/lib/claude-process-manager.ts +6 -4
  40. package/apps/dashboard/lib/db-bridge.ts +32 -0
  41. package/apps/dashboard/lib/db.ts +159 -13
  42. package/apps/dashboard/lib/session-state-machine.ts +3 -0
  43. package/apps/dashboard/lib/session-stream-manager.ts +76 -13
  44. package/apps/dashboard/lib/tests.ts +3 -1
  45. package/apps/dashboard/next.config.js +19 -14
  46. package/apps/dashboard/package.json +3 -1
  47. package/apps/dashboard/scripts/upload-to-r2.js +89 -0
  48. package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
  49. package/apps/update-server/package.json +16 -0
  50. package/apps/update-server/schema.sql +31 -0
  51. package/apps/update-server/src/index.ts +1074 -0
  52. package/apps/update-server/tsconfig.json +16 -0
  53. package/apps/update-server/wrangler.toml +35 -0
  54. package/docs/bdd-guidance.md +390 -0
  55. package/jettypod.js +5 -4
  56. package/lib/migrations/027-plan-at-creation-column.js +31 -0
  57. package/lib/migrations/028-ready-for-review-column.js +27 -0
  58. package/lib/schema.js +3 -1
  59. package/lib/seed-onboarding.js +100 -68
  60. package/lib/work-commands/index.js +43 -13
  61. package/lib/work-tracking/index.js +46 -27
  62. package/package.json +1 -1
  63. package/skills-templates/bug-mode/SKILL.md +5 -11
  64. package/skills-templates/request-routing/SKILL.md +24 -11
  65. package/skills-templates/simple-improvement/SKILL.md +35 -19
  66. package/skills-templates/stable-mode/SKILL.md +5 -6
  67. package/templates/bdd-guidance.md +139 -0
  68. package/templates/bdd-scaffolding/wait.js +18 -0
  69. package/templates/bdd-scaffolding/world.js +19 -0
  70. package/.jettypod-backup/work.db +0 -0
  71. package/apps/dashboard/app/access-code/page.tsx +0 -110
  72. package/lib/discovery-checkpoint.js +0 -123
  73. 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
- this.callbacks.onStateChange?.(this.getState());
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 for 5 seconds
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._status = 'creating';
515
- this.notifyStateChange();
516
-
517
- // Transition to streaming after 5 seconds
518
- this.creatingStatusTimeout = setTimeout(() => {
519
- if (this._status === 'creating') {
520
- this._status = 'streaming';
521
- this.notifyStateChange();
522
- }
523
- }, 5000);
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, context }, callback) => {
25
- // Externalize any require that goes to jettypod lib (dynamic requires)
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
- const absolutePath = path.join(jettypodLibPath, moduleName);
30
- return callback(null, `commonjs ${absolutePath}`);
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 - it uses dynamic require() to load
33
- // migration files at runtime, which webpack replaces with a dead stub
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
- const absolutePath = path.resolve(context || __dirname, request);
36
- return callback(null, `commonjs ${absolutePath}`);
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();