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
|
@@ -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,11 +46,12 @@ 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 {
|
|
46
53
|
onStateChange?: (state: StreamState) => void;
|
|
47
|
-
onWorkItemCreated?: (workItemId: number, title: string) => void;
|
|
54
|
+
onWorkItemCreated?: (workItemId: number, title: string, sourceSessionId: string) => void;
|
|
48
55
|
onGate?: (gate: ClaudeMessage) => void;
|
|
49
56
|
onQuestion?: (gate: ClaudeMessage) => void;
|
|
50
57
|
}
|
|
@@ -188,6 +195,23 @@ function transformClaudeMessage(parsed: any): ClaudeMessage | null {
|
|
|
188
195
|
timestamp,
|
|
189
196
|
};
|
|
190
197
|
|
|
198
|
+
// Claude CLI v2.1.49+ may emit streaming content block deltas
|
|
199
|
+
case 'content_block_delta': {
|
|
200
|
+
const delta = parsed.delta as { type?: string; text?: string } | undefined;
|
|
201
|
+
if (delta?.type === 'text_delta' && delta.text) {
|
|
202
|
+
return { type: 'assistant', content: delta.text, timestamp };
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
case 'content_block_start':
|
|
208
|
+
case 'content_block_stop':
|
|
209
|
+
case 'message_start':
|
|
210
|
+
case 'message_delta':
|
|
211
|
+
case 'message_stop':
|
|
212
|
+
// Streaming lifecycle events — no user-visible content
|
|
213
|
+
return null;
|
|
214
|
+
|
|
191
215
|
default:
|
|
192
216
|
return null;
|
|
193
217
|
}
|
|
@@ -266,9 +290,14 @@ export class SessionStreamManager {
|
|
|
266
290
|
private _isReconnecting: boolean = false;
|
|
267
291
|
private _reconnectAttempt: number = 0;
|
|
268
292
|
private _narratedMode: boolean = true;
|
|
293
|
+
private _userToggledNarratedMode: boolean = false;
|
|
269
294
|
private _pendingQuestion: ClaudeMessage | null = null;
|
|
295
|
+
private _queuedMessage: QueuedMessage | null = null;
|
|
270
296
|
private _isFirstMessage: boolean = true;
|
|
271
297
|
|
|
298
|
+
// Notification batching — coalesces rapid state changes into one callback per frame
|
|
299
|
+
private _notifyPending: boolean = false;
|
|
300
|
+
|
|
272
301
|
// Control
|
|
273
302
|
private abortController: AbortController | null = null;
|
|
274
303
|
private creatingStatusTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -278,6 +307,7 @@ export class SessionStreamManager {
|
|
|
278
307
|
// Last request (for retry/reconnect)
|
|
279
308
|
private lastMessage: string | null = null;
|
|
280
309
|
private lastImages: AttachedImage[] | undefined = undefined;
|
|
310
|
+
private _lastWasHidden: boolean = false;
|
|
281
311
|
|
|
282
312
|
// Callbacks
|
|
283
313
|
private callbacks: StreamManagerCallbacks;
|
|
@@ -327,11 +357,43 @@ export class SessionStreamManager {
|
|
|
327
357
|
return this._pendingQuestion;
|
|
328
358
|
}
|
|
329
359
|
|
|
360
|
+
get queuedMessage(): QueuedMessage | null {
|
|
361
|
+
return this._queuedMessage;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Queue a message to be sent after the current stream completes.
|
|
366
|
+
* Only one message can be queued at a time (last one wins).
|
|
367
|
+
*/
|
|
368
|
+
queueMessage(message: string, images?: AttachedImage[]): void {
|
|
369
|
+
this._queuedMessage = { message, images };
|
|
370
|
+
this.notifyStateChange();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Clear a queued message (e.g., if user stops the stream).
|
|
375
|
+
*/
|
|
376
|
+
clearQueuedMessage(): void {
|
|
377
|
+
this._queuedMessage = null;
|
|
378
|
+
this.notifyStateChange();
|
|
379
|
+
}
|
|
380
|
+
|
|
330
381
|
setNarratedMode(enabled: boolean): void {
|
|
331
382
|
this._narratedMode = enabled;
|
|
383
|
+
this._userToggledNarratedMode = true;
|
|
332
384
|
this.notifyStateChange();
|
|
333
385
|
}
|
|
334
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Update narrated mode without firing notifyStateChange.
|
|
389
|
+
* Used when React state is the source of truth (e.g., user toggle)
|
|
390
|
+
* to keep the stream manager in sync without overwriting React state.
|
|
391
|
+
*/
|
|
392
|
+
setNarratedModeQuiet(enabled: boolean): void {
|
|
393
|
+
this._narratedMode = enabled;
|
|
394
|
+
this._userToggledNarratedMode = true;
|
|
395
|
+
}
|
|
396
|
+
|
|
335
397
|
/**
|
|
336
398
|
* Answer a pending question gate by clearing it and sending the selection as a message
|
|
337
399
|
*/
|
|
@@ -351,6 +413,7 @@ export class SessionStreamManager {
|
|
|
351
413
|
isReconnecting: this._isReconnecting,
|
|
352
414
|
reconnectAttempt: this._reconnectAttempt,
|
|
353
415
|
narratedMode: this._narratedMode,
|
|
416
|
+
queuedMessage: this._queuedMessage,
|
|
354
417
|
};
|
|
355
418
|
}
|
|
356
419
|
|
|
@@ -359,7 +422,20 @@ export class SessionStreamManager {
|
|
|
359
422
|
// -------------------------------------------------------------------------
|
|
360
423
|
|
|
361
424
|
private notifyStateChange(): void {
|
|
362
|
-
|
|
425
|
+
if (!this._notifyPending) {
|
|
426
|
+
this._notifyPending = true;
|
|
427
|
+
const notify = () => {
|
|
428
|
+
this._notifyPending = false;
|
|
429
|
+
this.callbacks.onStateChange?.(this.getState());
|
|
430
|
+
};
|
|
431
|
+
// Batch rapid state changes (e.g., multiple stream events per frame) into one callback.
|
|
432
|
+
// requestAnimationFrame syncs with display refresh (~16ms); queueMicrotask as fallback.
|
|
433
|
+
if (typeof requestAnimationFrame !== 'undefined') {
|
|
434
|
+
requestAnimationFrame(notify);
|
|
435
|
+
} else {
|
|
436
|
+
queueMicrotask(notify);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
363
439
|
}
|
|
364
440
|
|
|
365
441
|
setMessages(messages: ClaudeMessage[]): void {
|
|
@@ -401,9 +477,16 @@ export class SessionStreamManager {
|
|
|
401
477
|
// Check for gate markers in tool results and text content
|
|
402
478
|
const gate = extractGateFromMessage(message);
|
|
403
479
|
if (gate) {
|
|
480
|
+
// Skip if the previous gate has the same gateType (prevents duplicate consecutive cards)
|
|
481
|
+
const lastGate = [...this._messages].reverse().find(m => m.type === 'gate');
|
|
482
|
+
if (lastGate && lastGate.gateType === gate.gateType) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
404
485
|
// Add the gate message instead of (or in addition to) the raw message
|
|
405
486
|
this._messages = [...this._messages, gate];
|
|
406
|
-
this.
|
|
487
|
+
if (!this._userToggledNarratedMode) {
|
|
488
|
+
this._narratedMode = true; // Auto-enable narrated mode on first gate
|
|
489
|
+
}
|
|
407
490
|
this.callbacks.onGate?.(gate);
|
|
408
491
|
|
|
409
492
|
// Question gates pause the workflow for user input
|
|
@@ -471,18 +554,19 @@ export class SessionStreamManager {
|
|
|
471
554
|
this.reconnectTimeout = setTimeout(() => {
|
|
472
555
|
if (this.stopped || !this.lastMessage) return;
|
|
473
556
|
|
|
474
|
-
// Remove the user message we added (will be re-added on retry)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
557
|
+
// Remove the user message we added (will be re-added on retry) — skip if hidden
|
|
558
|
+
if (!this._lastWasHidden) {
|
|
559
|
+
const lastUserIndex = this._messages.findLastIndex(m => m.type === 'user');
|
|
560
|
+
if (lastUserIndex >= 0) {
|
|
561
|
+
this._messages = this._messages.slice(0, lastUserIndex);
|
|
562
|
+
}
|
|
479
563
|
}
|
|
480
564
|
|
|
481
565
|
this._isReconnecting = false;
|
|
482
566
|
this.notifyStateChange();
|
|
483
567
|
|
|
484
568
|
// Retry the request
|
|
485
|
-
this.sendMessage(this.lastMessage, this.lastImages);
|
|
569
|
+
this.sendMessage(this.lastMessage, this.lastImages, { hidden: this._lastWasHidden });
|
|
486
570
|
}, delay);
|
|
487
571
|
}
|
|
488
572
|
|
|
@@ -491,36 +575,48 @@ export class SessionStreamManager {
|
|
|
491
575
|
// -------------------------------------------------------------------------
|
|
492
576
|
|
|
493
577
|
/**
|
|
494
|
-
* Send a message to Claude and stream the response
|
|
578
|
+
* Send a message to Claude and stream the response.
|
|
579
|
+
* When hidden is true, the user message is not added to visible messages —
|
|
580
|
+
* used for conversational sessions where Claude should speak first.
|
|
495
581
|
*/
|
|
496
|
-
async sendMessage(message: string, images?: AttachedImage[]): Promise<void> {
|
|
582
|
+
async sendMessage(message: string, images?: AttachedImage[], options?: { hidden?: boolean }): Promise<void> {
|
|
583
|
+
const hidden = options?.hidden ?? false;
|
|
497
584
|
this.stopped = false;
|
|
498
585
|
|
|
499
586
|
// Store for potential retry/reconnect
|
|
500
587
|
this.lastMessage = message;
|
|
501
588
|
this.lastImages = images;
|
|
589
|
+
this._lastWasHidden = hidden;
|
|
590
|
+
|
|
591
|
+
// Add user message immediately (unless hidden — conversational system instructions)
|
|
592
|
+
if (!hidden) {
|
|
593
|
+
const userMessage: ClaudeMessage = {
|
|
594
|
+
type: 'user',
|
|
595
|
+
content: message,
|
|
596
|
+
timestamp: Date.now(),
|
|
597
|
+
};
|
|
598
|
+
this.addMessage(userMessage);
|
|
599
|
+
}
|
|
502
600
|
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
type: 'user',
|
|
506
|
-
content: message,
|
|
507
|
-
timestamp: Date.now(),
|
|
508
|
-
};
|
|
509
|
-
this.addMessage(userMessage);
|
|
510
|
-
|
|
511
|
-
// For first message in a new session, show "creating" status for 5 seconds
|
|
601
|
+
// For first message in a new session, show "creating" status
|
|
602
|
+
// Conversational sessions skip the 5s delay and go straight to streaming
|
|
512
603
|
if (this._isFirstMessage) {
|
|
513
604
|
this._isFirstMessage = false;
|
|
514
|
-
this.
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
605
|
+
if (this.sessionContext.conversational) {
|
|
606
|
+
this._status = 'streaming';
|
|
607
|
+
this.notifyStateChange();
|
|
608
|
+
} else {
|
|
609
|
+
this._status = 'creating';
|
|
610
|
+
this.notifyStateChange();
|
|
611
|
+
|
|
612
|
+
// Transition to streaming after 5 seconds
|
|
613
|
+
this.creatingStatusTimeout = setTimeout(() => {
|
|
614
|
+
if (this._status === 'creating') {
|
|
615
|
+
this._status = 'streaming';
|
|
616
|
+
this.notifyStateChange();
|
|
617
|
+
}
|
|
618
|
+
}, 5000);
|
|
619
|
+
}
|
|
524
620
|
} else {
|
|
525
621
|
// Set status to streaming
|
|
526
622
|
this._status = 'streaming';
|
|
@@ -541,7 +637,6 @@ export class SessionStreamManager {
|
|
|
541
637
|
},
|
|
542
638
|
body: JSON.stringify({
|
|
543
639
|
message,
|
|
544
|
-
conversationHistory: this._messages.slice(0, -1), // Exclude the user message we just added
|
|
545
640
|
images: images?.map(img => ({
|
|
546
641
|
type: img.type,
|
|
547
642
|
data: img.dataUrl,
|
|
@@ -618,7 +713,7 @@ export class SessionStreamManager {
|
|
|
618
713
|
if (this.sessionContext.standalone && this.callbacks.onWorkItemCreated) {
|
|
619
714
|
const created = checkMessageForWorkItemCreation(claudeMessage);
|
|
620
715
|
if (created) {
|
|
621
|
-
this.callbacks.onWorkItemCreated(created.id, created.title);
|
|
716
|
+
this.callbacks.onWorkItemCreated(created.id, created.title, this.sessionContext.workItemId);
|
|
622
717
|
}
|
|
623
718
|
}
|
|
624
719
|
}
|
|
@@ -667,6 +762,7 @@ export class SessionStreamManager {
|
|
|
667
762
|
this._status = 'idle';
|
|
668
763
|
this._isReconnecting = false;
|
|
669
764
|
this._reconnectAttempt = 0;
|
|
765
|
+
this._queuedMessage = null; // Clear queue on stop
|
|
670
766
|
this.notifyStateChange();
|
|
671
767
|
}
|
|
672
768
|
|
|
@@ -681,6 +777,7 @@ export class SessionStreamManager {
|
|
|
681
777
|
this._canRetry = false;
|
|
682
778
|
this._isReconnecting = false;
|
|
683
779
|
this._reconnectAttempt = 0;
|
|
780
|
+
this._queuedMessage = null;
|
|
684
781
|
this.lastMessage = null;
|
|
685
782
|
this.lastImages = undefined;
|
|
686
783
|
this.notifyStateChange();
|
|
@@ -700,16 +797,18 @@ export class SessionStreamManager {
|
|
|
700
797
|
this._error = null;
|
|
701
798
|
this._canRetry = false;
|
|
702
799
|
|
|
703
|
-
// Remove the failed user message (will be re-added on retry)
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
800
|
+
// Remove the failed user message (will be re-added on retry) — skip if hidden
|
|
801
|
+
if (!this._lastWasHidden) {
|
|
802
|
+
const lastUserIndex = this._messages.findLastIndex(m => m.type === 'user');
|
|
803
|
+
if (lastUserIndex >= 0) {
|
|
804
|
+
this._messages = this._messages.slice(0, lastUserIndex);
|
|
805
|
+
}
|
|
707
806
|
}
|
|
708
807
|
|
|
709
808
|
this.notifyStateChange();
|
|
710
809
|
|
|
711
810
|
// Resend the last message
|
|
712
|
-
this.sendMessage(this.lastMessage, this.lastImages);
|
|
811
|
+
this.sendMessage(this.lastMessage, this.lastImages, { hidden: this._lastWasHidden });
|
|
713
812
|
}
|
|
714
813
|
|
|
715
814
|
/**
|
|
@@ -717,6 +816,11 @@ export class SessionStreamManager {
|
|
|
717
816
|
* This adds the gate directly to the message list without parsing from stream text.
|
|
718
817
|
*/
|
|
719
818
|
injectGate(gateType: string, gateData: Record<string, unknown> = {}): void {
|
|
819
|
+
// Skip if the previous gate has the same type (prevents duplicate consecutive cards)
|
|
820
|
+
const lastGate = [...this._messages].reverse().find(m => m.type === 'gate');
|
|
821
|
+
if (lastGate && lastGate.gateType === gateType) {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
720
824
|
const gate: ClaudeMessage = {
|
|
721
825
|
type: 'gate',
|
|
722
826
|
gateType,
|
|
@@ -724,7 +828,9 @@ export class SessionStreamManager {
|
|
|
724
828
|
timestamp: Date.now(),
|
|
725
829
|
};
|
|
726
830
|
this._messages = [...this._messages, gate];
|
|
727
|
-
this.
|
|
831
|
+
if (!this._userToggledNarratedMode) {
|
|
832
|
+
this._narratedMode = true;
|
|
833
|
+
}
|
|
728
834
|
this.callbacks.onGate?.(gate);
|
|
729
835
|
this.notifyStateChange();
|
|
730
836
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Design system elevation tokens — shared across all components. */
|
|
2
|
+
export const shadow = {
|
|
3
|
+
sm: '0 1px 2px rgba(0,0,0,0.04), 0 2px 6px rgba(0,0,0,0.02)',
|
|
4
|
+
md: '0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.03)',
|
|
5
|
+
lg: '0 4px 12px rgba(0,0,0,0.06), 0 12px 28px rgba(0,0,0,0.05)',
|
|
6
|
+
overlay: '0 8px 24px rgba(0,0,0,0.12), 0 16px 48px rgba(0,0,0,0.08)',
|
|
7
|
+
};
|
|
@@ -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,
|
|
@@ -4,3 +4,9 @@ import { twMerge } from "tailwind-merge"
|
|
|
4
4
|
export function cn(...inputs: ClassValue[]) {
|
|
5
5
|
return twMerge(clsx(inputs))
|
|
6
6
|
}
|
|
7
|
+
|
|
8
|
+
export function getWebSocketUrl(): string {
|
|
9
|
+
return typeof window !== 'undefined'
|
|
10
|
+
? `ws://${window.location.hostname}:47808`
|
|
11
|
+
: 'ws://localhost:47808';
|
|
12
|
+
}
|
|
@@ -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
|
|
@@ -18,22 +14,47 @@ const nextConfig = {
|
|
|
18
14
|
// The webpack externals below properly externalize the jettypod lib chain
|
|
19
15
|
|
|
20
16
|
webpack: (config, { isServer }) => {
|
|
17
|
+
// Split @dnd-kit into a separate chunk so it doesn't block initial page parse.
|
|
18
|
+
// The chunk loads in parallel and is only needed when KanbanBoard renders.
|
|
19
|
+
if (!isServer) {
|
|
20
|
+
config.optimization = config.optimization || {};
|
|
21
|
+
config.optimization.splitChunks = config.optimization.splitChunks || {};
|
|
22
|
+
config.optimization.splitChunks.cacheGroups = {
|
|
23
|
+
...config.optimization.splitChunks.cacheGroups,
|
|
24
|
+
dndkit: {
|
|
25
|
+
test: /[\\/]node_modules[\\/]@dnd-kit[\\/]/,
|
|
26
|
+
name: 'dnd-kit',
|
|
27
|
+
chunks: 'all',
|
|
28
|
+
priority: 30,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
if (isServer) {
|
|
22
|
-
// Externalize the jettypod lib using absolute path resolution
|
|
23
34
|
config.externals = config.externals || [];
|
|
24
|
-
config.externals.push(({ request
|
|
25
|
-
// Externalize
|
|
35
|
+
config.externals.push(({ request }, callback) => {
|
|
36
|
+
// Externalize worktree-facade with RUNTIME path resolution.
|
|
37
|
+
// Uses 'var' external type so the path expression evaluates at runtime,
|
|
38
|
+
// not at build time (which would bake in the build machine's absolute path).
|
|
39
|
+
// Packaged app: JETTYPOD_RESOURCES_PATH/bin/lib/<module>
|
|
40
|
+
// Dev mode: process.cwd()/../../lib/<module>
|
|
26
41
|
if (request && request.includes('lib/worktree-facade')) {
|
|
27
|
-
// Use absolute path to jettypod lib
|
|
28
42
|
const moduleName = request.split('lib/')[1];
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
return callback(null,
|
|
44
|
+
`var require(process.env.JETTYPOD_RESOURCES_PATH ` +
|
|
45
|
+
`? require('path').join(process.env.JETTYPOD_RESOURCES_PATH, 'bin', 'lib', '${moduleName}') ` +
|
|
46
|
+
`: require('path').resolve(process.cwd(), '../../lib', '${moduleName}'))`
|
|
47
|
+
);
|
|
31
48
|
}
|
|
32
|
-
// Externalize run-migrations.js
|
|
33
|
-
//
|
|
49
|
+
// Externalize run-migrations.js with RUNTIME path resolution.
|
|
50
|
+
// It uses dynamic require() to load migration files, which webpack
|
|
51
|
+
// replaces with a dead stub if bundled.
|
|
52
|
+
// In both packaged and dev: process.cwd() is the dashboard dir,
|
|
53
|
+
// and run-migrations.js is at lib/run-migrations.js relative to it.
|
|
34
54
|
if (request && request.includes('run-migrations')) {
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
return callback(null,
|
|
56
|
+
`var require(require('path').join(process.cwd(), 'lib', 'run-migrations'))`
|
|
57
|
+
);
|
|
37
58
|
}
|
|
38
59
|
callback(undefined);
|
|
39
60
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dashboard",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.4.120",
|
|
4
4
|
"private": true,
|
|
5
5
|
"main": "electron/main.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "next dev --webpack",
|
|
7
|
+
"dev": "concurrently -n next,ws -c blue,green \"next dev --webpack\" \"node scripts/ws-server.js\"",
|
|
8
8
|
"build": "next build --webpack",
|
|
9
9
|
"start": "next start",
|
|
10
10
|
"lint": "eslint",
|
|
@@ -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",
|
|
@@ -45,6 +47,7 @@
|
|
|
45
47
|
"@types/node": "^20",
|
|
46
48
|
"@types/react": "^19",
|
|
47
49
|
"@types/react-dom": "^19",
|
|
50
|
+
"concurrently": "^9.2.1",
|
|
48
51
|
"electron": "^32.0.0",
|
|
49
52
|
"electron-builder": "^26.4.0",
|
|
50
53
|
"eslint": "^9",
|