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
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type ClaudeMessage,
|
|
6
6
|
type StreamStatus,
|
|
7
7
|
type StreamState,
|
|
8
|
+
type QueuedMessage,
|
|
8
9
|
} from '../lib/session-stream-manager';
|
|
9
10
|
import { getRegistry } from '../lib/stream-manager-registry';
|
|
10
11
|
import {
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
} from '../lib/session-state-machine';
|
|
22
23
|
import { getMessageBuffer } from '../lib/message-buffer';
|
|
23
24
|
import { useToast } from '../components/Toast';
|
|
25
|
+
import { useUsage } from './UsageContext';
|
|
24
26
|
import type { SessionItem } from '../components/SessionList';
|
|
25
27
|
|
|
26
28
|
// Re-export ClaudeMessage for consumers
|
|
@@ -58,17 +60,20 @@ interface ClaudeSessionContextValue {
|
|
|
58
60
|
error: string | null;
|
|
59
61
|
exitCode: number | null;
|
|
60
62
|
canRetry: boolean;
|
|
63
|
+
queuedMessage: QueuedMessage | null;
|
|
61
64
|
narratedMode: boolean;
|
|
62
65
|
toggleNarratedMode: () => void;
|
|
63
66
|
|
|
64
67
|
// Session actions
|
|
65
|
-
openSession: (id: string, title: string, type?: string) => void;
|
|
68
|
+
openSession: (id: string, title: string, type?: string, conversational?: boolean, description?: string | null) => void;
|
|
66
69
|
switchSession: (id: string) => void;
|
|
67
70
|
closeSession: (sessionId: string) => void;
|
|
68
71
|
openSessionPanel: () => void;
|
|
69
72
|
createNewSession: () => Promise<void>;
|
|
70
73
|
createAddToBacklogSession: () => Promise<void>;
|
|
71
74
|
createRunScenarioSession: (featureFile: string, scenarioTitle: string) => Promise<void>;
|
|
75
|
+
createFixScenarioSession: (featureFile: string, scenarioTitle: string, error: string, failedStep?: string, steps?: string[]) => Promise<void>;
|
|
76
|
+
createWelcomeSession: () => Promise<void>;
|
|
72
77
|
|
|
73
78
|
// Stream actions
|
|
74
79
|
sendMessage: (message: string) => void;
|
|
@@ -100,6 +105,7 @@ const ACTIVE_SESSION_KEY = 'jettypod-active-session-id';
|
|
|
100
105
|
export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
101
106
|
// Toast for user notifications
|
|
102
107
|
const { showToast } = useToast();
|
|
108
|
+
const { allowed: usageAllowed, refresh: refreshUsage } = useUsage();
|
|
103
109
|
|
|
104
110
|
// Get registry singleton (stable across renders)
|
|
105
111
|
const registry = getRegistry();
|
|
@@ -193,6 +199,17 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
193
199
|
machine.send('CONNECTED');
|
|
194
200
|
} else if (state.status === 'done') {
|
|
195
201
|
machine.send('COMPLETE');
|
|
202
|
+
// Auto-send queued message after stream completes
|
|
203
|
+
if (state.queuedMessage) {
|
|
204
|
+
const streamManager = registry.get(sessionId);
|
|
205
|
+
if (streamManager && machine.canSend('SEND')) {
|
|
206
|
+
const { message, images } = state.queuedMessage;
|
|
207
|
+
streamManager.clearQueuedMessage();
|
|
208
|
+
machine.send('SEND');
|
|
209
|
+
setSessionStreaming(sessionRefs, sessionId, true);
|
|
210
|
+
streamManager.sendMessage(message, images);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
196
213
|
} else if (state.status === 'error') {
|
|
197
214
|
machine.send('ERROR');
|
|
198
215
|
} else if (state.status === 'idle' && machine.state !== 'idle') {
|
|
@@ -253,11 +270,12 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
253
270
|
const getOrCreateStreamManager = useCallback((
|
|
254
271
|
sessionId: string,
|
|
255
272
|
standalone: boolean,
|
|
256
|
-
onWorkItemCreated?: (workItemId: number, title: string) => void
|
|
273
|
+
onWorkItemCreated?: (workItemId: number, title: string) => void,
|
|
274
|
+
conversational?: boolean
|
|
257
275
|
) => {
|
|
258
276
|
// Registry handles idempotent creation and state change events
|
|
259
277
|
return registry.create(sessionId, {
|
|
260
|
-
context: { workItemId: sessionId, standalone },
|
|
278
|
+
context: { workItemId: sessionId, standalone, conversational },
|
|
261
279
|
callbacks: { onWorkItemCreated },
|
|
262
280
|
});
|
|
263
281
|
}, [registry]);
|
|
@@ -267,6 +285,9 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
267
285
|
// firing this callback, so we don't need to re-check sessions here (which
|
|
268
286
|
// would fail due to stale closure over the sessions Map).
|
|
269
287
|
const handleWorkItemCreated = useCallback(async (workItemId: number, title: string) => {
|
|
288
|
+
// Refresh usage count from local DB (work item now exists in work.db)
|
|
289
|
+
refreshUsage();
|
|
290
|
+
|
|
270
291
|
// Use sync ref to get current active session (avoids stale closure)
|
|
271
292
|
const currentActiveId = sessionRefs.activeSessionId.current;
|
|
272
293
|
if (!currentActiveId) return;
|
|
@@ -325,7 +346,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
325
346
|
} catch (err) {
|
|
326
347
|
console.error('Failed to link session to work item:', err);
|
|
327
348
|
}
|
|
328
|
-
}, [sessionRefs, registry, setActiveSessionId]);
|
|
349
|
+
}, [sessionRefs, registry, setActiveSessionId, refreshUsage]);
|
|
329
350
|
|
|
330
351
|
// Load persisted sessions from backend on mount
|
|
331
352
|
useEffect(() => {
|
|
@@ -397,7 +418,8 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
397
418
|
status: 'idle',
|
|
398
419
|
error: null,
|
|
399
420
|
exitCode: null,
|
|
400
|
-
|
|
421
|
+
// Welcome session shows static content — detail view shows all messages
|
|
422
|
+
narratedMode: session.title !== 'Welcome',
|
|
401
423
|
// Stream manager created lazily when session becomes active
|
|
402
424
|
});
|
|
403
425
|
}
|
|
@@ -436,7 +458,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
436
458
|
// Open or create a session for a work item
|
|
437
459
|
// With per-session streams, no need to stop other streams or track ownership
|
|
438
460
|
// Auto-sends an initial message so Claude starts working immediately
|
|
439
|
-
const openSession = useCallback((id: string, title: string, type?: string) => {
|
|
461
|
+
const openSession = useCallback((id: string, title: string, type?: string, conversational?: boolean, description?: string | null) => {
|
|
440
462
|
if (sessions.has(id)) {
|
|
441
463
|
// Switching to existing session - ensure it has a stream manager
|
|
442
464
|
const session = sessions.get(id)!;
|
|
@@ -447,8 +469,14 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
447
469
|
return;
|
|
448
470
|
}
|
|
449
471
|
|
|
450
|
-
//
|
|
451
|
-
|
|
472
|
+
// Gate new session creation on usage limits
|
|
473
|
+
if (!usageAllowed) {
|
|
474
|
+
showToast('Weekly limit reached. Upgrade to continue using Claude features.', 'error');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Create stream manager in registry — pass conversational flag for skip-delay behavior
|
|
479
|
+
const streamManager = getOrCreateStreamManager(id, false, undefined, conversational);
|
|
452
480
|
registry.acquire(id);
|
|
453
481
|
|
|
454
482
|
const newSession: Session = {
|
|
@@ -472,12 +500,17 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
472
500
|
setClaudePanelOpen(true);
|
|
473
501
|
|
|
474
502
|
// Auto-send initial message so Claude starts working immediately
|
|
475
|
-
//
|
|
476
|
-
const initialMessage =
|
|
503
|
+
// Conversational chores include their description so Claude has full context
|
|
504
|
+
const initialMessage = conversational
|
|
505
|
+
? `This is a conversation — no code, no worktrees. Just chat with me naturally.\n\n${description || title}`
|
|
506
|
+
: `I'm starting work on ${type || 'work item'} #${id}: ${title}`;
|
|
477
507
|
const machine = getStateMachine(id);
|
|
478
508
|
machine.send('SEND');
|
|
479
509
|
streamManager.sendMessage(initialMessage);
|
|
480
|
-
|
|
510
|
+
|
|
511
|
+
// Refresh usage so UI reflects the new session immediately
|
|
512
|
+
refreshUsage();
|
|
513
|
+
}, [sessions, registry, getOrCreateStreamManager, ensureStreamManager, setActiveSessionId, getStateMachine, usageAllowed, showToast, refreshUsage]);
|
|
481
514
|
|
|
482
515
|
// Track target session during async switch operations (for tab-switching UX, not streaming)
|
|
483
516
|
const switchingToRef = useRef<string | null>(null);
|
|
@@ -670,6 +703,11 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
670
703
|
// Create a new standalone session
|
|
671
704
|
// With per-session streams, each new session gets its own stream manager in registry
|
|
672
705
|
const createNewSession = useCallback(async () => {
|
|
706
|
+
if (!usageAllowed) {
|
|
707
|
+
showToast('Weekly limit reached. Upgrade to continue using Claude features.', 'error');
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
673
711
|
try {
|
|
674
712
|
const response = await fetch('/api/claude/sessions', {
|
|
675
713
|
method: 'POST',
|
|
@@ -721,10 +759,13 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
721
759
|
|
|
722
760
|
setActiveSessionId(id);
|
|
723
761
|
setClaudePanelOpen(true);
|
|
762
|
+
|
|
763
|
+
// Refresh usage so UI reflects the new session immediately
|
|
764
|
+
refreshUsage();
|
|
724
765
|
} catch (err) {
|
|
725
766
|
console.error('[ClaudeSessionContext] Failed to create session:', err);
|
|
726
767
|
}
|
|
727
|
-
}, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId]);
|
|
768
|
+
}, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, usageAllowed, refreshUsage]);
|
|
728
769
|
|
|
729
770
|
// Open the session panel (restore last session or create new)
|
|
730
771
|
const openSessionPanel = useCallback(() => {
|
|
@@ -758,29 +799,38 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
758
799
|
return;
|
|
759
800
|
}
|
|
760
801
|
|
|
761
|
-
// Validate state transition via state machine
|
|
762
802
|
const machine = getStateMachine(currentActiveId);
|
|
763
|
-
|
|
764
|
-
|
|
803
|
+
|
|
804
|
+
// If we can send directly, do so
|
|
805
|
+
if (machine.canSend('SEND')) {
|
|
806
|
+
const streamManager = registry.get(currentActiveId);
|
|
807
|
+
if (!streamManager) {
|
|
808
|
+
// Lazy create if needed
|
|
809
|
+
const session = sessions.get(currentActiveId);
|
|
810
|
+
if (!session) return;
|
|
811
|
+
const mgr = ensureStreamManager(currentActiveId, session);
|
|
812
|
+
machine.send('SEND');
|
|
813
|
+
mgr.sendMessage(message);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
machine.send('SEND');
|
|
818
|
+
setSessionStreaming(sessionRefs, currentActiveId, true);
|
|
819
|
+
streamManager.sendMessage(message);
|
|
765
820
|
return;
|
|
766
821
|
}
|
|
767
822
|
|
|
768
|
-
//
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
machine.send('SEND'); // Transition to connecting
|
|
776
|
-
mgr.sendMessage(message);
|
|
823
|
+
// If streaming/connecting, queue the message for later
|
|
824
|
+
if (machine.canSend('QUEUE')) {
|
|
825
|
+
const streamManager = registry.get(currentActiveId);
|
|
826
|
+
if (!streamManager) return;
|
|
827
|
+
|
|
828
|
+
machine.send('QUEUE');
|
|
829
|
+
streamManager.queueMessage(message);
|
|
777
830
|
return;
|
|
778
831
|
}
|
|
779
832
|
|
|
780
|
-
|
|
781
|
-
machine.send('SEND');
|
|
782
|
-
setSessionStreaming(sessionRefs, currentActiveId, true);
|
|
783
|
-
streamManager.sendMessage(message);
|
|
833
|
+
console.warn(`[ClaudeSessionContext] Cannot send message: invalid state transition from ${machine.state}`);
|
|
784
834
|
}, [sessionRefs, registry, sessions, ensureStreamManager, getStateMachine]);
|
|
785
835
|
|
|
786
836
|
// Retry via the active session's stream manager
|
|
@@ -839,6 +889,11 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
839
889
|
// Create an "Add to Backlog" session with initial assistant message
|
|
840
890
|
// With per-session streams, each session has its own manager in registry
|
|
841
891
|
const createAddToBacklogSession = useCallback(async () => {
|
|
892
|
+
if (!usageAllowed) {
|
|
893
|
+
showToast('Weekly limit reached. Upgrade to continue using Claude features.', 'error');
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
842
897
|
try {
|
|
843
898
|
const response = await fetch('/api/claude/sessions', {
|
|
844
899
|
method: 'POST',
|
|
@@ -899,13 +954,116 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
899
954
|
|
|
900
955
|
setActiveSessionId(id);
|
|
901
956
|
setClaudePanelOpen(true);
|
|
957
|
+
|
|
958
|
+
// Refresh usage so UI reflects the new session immediately
|
|
959
|
+
refreshUsage();
|
|
902
960
|
} catch (err) {
|
|
903
961
|
console.error('[ClaudeSessionContext] Failed to create backlog session:', err);
|
|
904
962
|
}
|
|
963
|
+
}, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, usageAllowed, refreshUsage]);
|
|
964
|
+
|
|
965
|
+
// Create a welcome session for blank projects with initial assistant message
|
|
966
|
+
const createWelcomeSession = useCallback(async () => {
|
|
967
|
+
try {
|
|
968
|
+
const response = await fetch('/api/claude/sessions', {
|
|
969
|
+
method: 'POST',
|
|
970
|
+
headers: { 'Content-Type': 'application/json' },
|
|
971
|
+
body: JSON.stringify({ title: 'Welcome' }),
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
if (!response.ok) {
|
|
975
|
+
const errorData = await response.json();
|
|
976
|
+
if (errorData.code === 'SESSION_LIMIT_REACHED') {
|
|
977
|
+
console.warn(`[ClaudeSessionContext] ${errorData.error}`);
|
|
978
|
+
showToast('Session limit reached. Close existing sessions to create new ones.', 'error');
|
|
979
|
+
}
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const { id, title } = await response.json();
|
|
984
|
+
|
|
985
|
+
setStandaloneSessions(prev => [...prev, {
|
|
986
|
+
id,
|
|
987
|
+
title,
|
|
988
|
+
featureId: null,
|
|
989
|
+
featureTitle: null,
|
|
990
|
+
}]);
|
|
991
|
+
|
|
992
|
+
const now = Date.now();
|
|
993
|
+
|
|
994
|
+
const greetingMessage: ClaudeMessage = {
|
|
995
|
+
type: 'assistant',
|
|
996
|
+
content: 'Ahoy. Your project is set up and ready to go.',
|
|
997
|
+
timestamp: now,
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
const backlogTip: ClaudeMessage = {
|
|
1001
|
+
type: 'gate',
|
|
1002
|
+
gateType: 'tip',
|
|
1003
|
+
gateData: {
|
|
1004
|
+
id: 'tip-backlog',
|
|
1005
|
+
icon: '📋',
|
|
1006
|
+
title: 'The Backlog',
|
|
1007
|
+
body: 'The backlog is where all your work items live.\n\n**Features** — new user-facing capabilities\n**Chores** — technical tasks\n**Bugs** — something broke\n**Epics** — a group of related features and chores',
|
|
1008
|
+
},
|
|
1009
|
+
timestamp: now + 1,
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
const workflowTip: ClaudeMessage = {
|
|
1013
|
+
type: 'gate',
|
|
1014
|
+
gateType: 'tip',
|
|
1015
|
+
gateData: {
|
|
1016
|
+
id: 'tip-workflow',
|
|
1017
|
+
icon: '🔄',
|
|
1018
|
+
title: 'How to Work',
|
|
1019
|
+
body: '**1. Start** — click \'start\' on a work item to open a chat\n**2. Chat** — work through it with Claude\n**3. Close** — close the chat and start what\'s next',
|
|
1020
|
+
},
|
|
1021
|
+
timestamp: now + 2,
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
const ctaMessage: ClaudeMessage = {
|
|
1025
|
+
type: 'assistant',
|
|
1026
|
+
content: 'Close this welcome chat and start your first onboarding chore\u2014**Align on the user journey**.',
|
|
1027
|
+
timestamp: now + 3,
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
const welcomeMessages = [greetingMessage, backlogTip, workflowTip, ctaMessage];
|
|
1031
|
+
|
|
1032
|
+
const streamManager = getOrCreateStreamManager(id, true, handleWorkItemCreated);
|
|
1033
|
+
registry.acquire(id);
|
|
1034
|
+
streamManager.setMessages(welcomeMessages);
|
|
1035
|
+
|
|
1036
|
+
const newSession: Session = {
|
|
1037
|
+
id,
|
|
1038
|
+
title,
|
|
1039
|
+
type: 'standalone',
|
|
1040
|
+
messages: welcomeMessages,
|
|
1041
|
+
status: 'idle',
|
|
1042
|
+
error: null,
|
|
1043
|
+
exitCode: null,
|
|
1044
|
+
narratedMode: false,
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
setSessions(prev => {
|
|
1048
|
+
const updated = new Map(prev);
|
|
1049
|
+
updated.set(id, newSession);
|
|
1050
|
+
return updated;
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
setActiveSessionId(id);
|
|
1054
|
+
setClaudePanelOpen(true);
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
console.error('[ClaudeSessionContext] Failed to create welcome session:', err);
|
|
1057
|
+
}
|
|
905
1058
|
}, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId]);
|
|
906
1059
|
|
|
907
1060
|
// Create a "Run Scenario" session with preloaded cucumber-js command
|
|
908
1061
|
const createRunScenarioSession = useCallback(async (featureFile: string, scenarioTitle: string) => {
|
|
1062
|
+
if (!usageAllowed) {
|
|
1063
|
+
showToast('Weekly limit reached. Upgrade to continue using Claude features.', 'error');
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
909
1067
|
try {
|
|
910
1068
|
const sessionTitle = `Run: ${scenarioTitle}`;
|
|
911
1069
|
const response = await fetch('/api/claude/sessions', {
|
|
@@ -969,11 +1127,113 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
969
1127
|
const machine = getStateMachine(id);
|
|
970
1128
|
machine.send('SEND');
|
|
971
1129
|
streamManager.sendMessage(userMessage.content!);
|
|
1130
|
+
|
|
1131
|
+
// Refresh usage so UI reflects the new session immediately
|
|
1132
|
+
refreshUsage();
|
|
972
1133
|
} catch (err) {
|
|
973
1134
|
console.error('[ClaudeSessionContext] Failed to create run scenario session:', err);
|
|
974
1135
|
showToast('Failed to create session. Please try again.', 'error');
|
|
975
1136
|
}
|
|
976
|
-
}, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, getStateMachine]);
|
|
1137
|
+
}, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, getStateMachine, usageAllowed, refreshUsage]);
|
|
1138
|
+
|
|
1139
|
+
// Create a "Fix Scenario" session with preloaded failure context
|
|
1140
|
+
const createFixScenarioSession = useCallback(async (featureFile: string, scenarioTitle: string, error: string, failedStep?: string, steps?: string[]) => {
|
|
1141
|
+
if (!usageAllowed) {
|
|
1142
|
+
showToast('Weekly limit reached. Upgrade to continue using Claude features.', 'error');
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
try {
|
|
1147
|
+
const sessionTitle = `Fix: ${scenarioTitle}`;
|
|
1148
|
+
const response = await fetch('/api/claude/sessions', {
|
|
1149
|
+
method: 'POST',
|
|
1150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1151
|
+
body: JSON.stringify({ title: sessionTitle }),
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
if (!response.ok) {
|
|
1155
|
+
const errorData = await response.json();
|
|
1156
|
+
if (errorData.code === 'SESSION_LIMIT_REACHED') {
|
|
1157
|
+
console.warn(`[ClaudeSessionContext] ${errorData.error}`);
|
|
1158
|
+
showToast('Session limit reached. Close existing sessions to create new ones.', 'error');
|
|
1159
|
+
} else {
|
|
1160
|
+
console.error('[ClaudeSessionContext] Failed to create fix scenario session:', errorData.error);
|
|
1161
|
+
showToast('Failed to create session. Please try again.', 'error');
|
|
1162
|
+
}
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const { id, title } = await response.json();
|
|
1167
|
+
|
|
1168
|
+
setStandaloneSessions(prev => [...prev, {
|
|
1169
|
+
id,
|
|
1170
|
+
title,
|
|
1171
|
+
featureId: null,
|
|
1172
|
+
featureTitle: null,
|
|
1173
|
+
}]);
|
|
1174
|
+
|
|
1175
|
+
const promptParts = [
|
|
1176
|
+
'A BDD scenario is failing and needs to be fixed.',
|
|
1177
|
+
'',
|
|
1178
|
+
`**Scenario:** ${scenarioTitle}`,
|
|
1179
|
+
`**Feature file:** ${featureFile}`,
|
|
1180
|
+
];
|
|
1181
|
+
|
|
1182
|
+
if (failedStep) {
|
|
1183
|
+
promptParts.push(`**Failed step:** ${failedStep}`);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
promptParts.push('**Error:**', '```', error, '```');
|
|
1187
|
+
|
|
1188
|
+
if (steps && steps.length > 0) {
|
|
1189
|
+
promptParts.push('', '**Scenario steps:**');
|
|
1190
|
+
steps.forEach(step => promptParts.push(` ${step}`));
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
promptParts.push('', 'Please investigate this failure, identify the root cause, and fix it.');
|
|
1194
|
+
|
|
1195
|
+
const userMessage: ClaudeMessage = {
|
|
1196
|
+
type: 'user',
|
|
1197
|
+
content: promptParts.join('\n'),
|
|
1198
|
+
timestamp: Date.now(),
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
const streamManager = getOrCreateStreamManager(id, true, handleWorkItemCreated);
|
|
1202
|
+
registry.acquire(id);
|
|
1203
|
+
streamManager.setMessages([userMessage]);
|
|
1204
|
+
|
|
1205
|
+
const newSession: Session = {
|
|
1206
|
+
id,
|
|
1207
|
+
title,
|
|
1208
|
+
type: 'standalone',
|
|
1209
|
+
messages: [userMessage],
|
|
1210
|
+
status: 'idle',
|
|
1211
|
+
error: null,
|
|
1212
|
+
exitCode: null,
|
|
1213
|
+
narratedMode: true,
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
setSessions(prev => {
|
|
1217
|
+
const updated = new Map(prev);
|
|
1218
|
+
updated.set(id, newSession);
|
|
1219
|
+
return updated;
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
setActiveSessionId(id);
|
|
1223
|
+
setClaudePanelOpen(true);
|
|
1224
|
+
|
|
1225
|
+
// Auto-send the fix request to Claude
|
|
1226
|
+
const machine = getStateMachine(id);
|
|
1227
|
+
machine.send('SEND');
|
|
1228
|
+
streamManager.sendMessage(userMessage.content!);
|
|
1229
|
+
|
|
1230
|
+
// Refresh usage so UI reflects the new session immediately
|
|
1231
|
+
refreshUsage();
|
|
1232
|
+
} catch (err) {
|
|
1233
|
+
console.error('[ClaudeSessionContext] Failed to create fix scenario session:', err);
|
|
1234
|
+
showToast('Failed to create session. Please try again.', 'error');
|
|
1235
|
+
}
|
|
1236
|
+
}, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, getStateMachine, usageAllowed, refreshUsage]);
|
|
977
1237
|
|
|
978
1238
|
// Setters for direct manipulation (e.g., restoring from DB)
|
|
979
1239
|
// These now work through the registry
|
|
@@ -1018,6 +1278,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1018
1278
|
error: activeSession?.error ?? null,
|
|
1019
1279
|
exitCode: activeSession?.exitCode ?? null,
|
|
1020
1280
|
canRetry: activeStreamManager?.canRetry ?? false,
|
|
1281
|
+
queuedMessage: activeStreamManager?.queuedMessage ?? null,
|
|
1021
1282
|
narratedMode: activeSession?.narratedMode ?? false,
|
|
1022
1283
|
toggleNarratedMode,
|
|
1023
1284
|
|
|
@@ -1029,6 +1290,8 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1029
1290
|
createNewSession,
|
|
1030
1291
|
createAddToBacklogSession,
|
|
1031
1292
|
createRunScenarioSession,
|
|
1293
|
+
createFixScenarioSession,
|
|
1294
|
+
createWelcomeSession,
|
|
1032
1295
|
|
|
1033
1296
|
// Stream actions (now go through registry)
|
|
1034
1297
|
sendMessage,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
|
|
4
|
+
import { useWebSocket, type WebSocketMessage } from '../hooks/useWebSocket';
|
|
5
|
+
|
|
6
|
+
const FREE_WEEKLY_LIMIT = 20;
|
|
7
|
+
|
|
8
|
+
interface UsageState {
|
|
9
|
+
used: number;
|
|
10
|
+
limit: number;
|
|
11
|
+
remaining: number;
|
|
12
|
+
allowed: boolean;
|
|
13
|
+
plan: string;
|
|
14
|
+
loading: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface UsageContextValue extends UsageState {
|
|
18
|
+
refresh: () => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const UsageContext = createContext<UsageContextValue | null>(null);
|
|
22
|
+
|
|
23
|
+
export function UsageProvider({ children }: { children: ReactNode }) {
|
|
24
|
+
const [state, setState] = useState<UsageState>({
|
|
25
|
+
used: 0,
|
|
26
|
+
limit: FREE_WEEKLY_LIMIT,
|
|
27
|
+
remaining: FREE_WEEKLY_LIMIT,
|
|
28
|
+
allowed: true,
|
|
29
|
+
plan: 'free',
|
|
30
|
+
loading: true,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const fetchUsage = useCallback(async () => {
|
|
34
|
+
// Get plan from auth (if Electron)
|
|
35
|
+
let plan = 'free';
|
|
36
|
+
try {
|
|
37
|
+
if (window.electronAPI?.isElectron) {
|
|
38
|
+
const status = await window.electronAPI.auth.getStatus();
|
|
39
|
+
if (status.authenticated && status.user) {
|
|
40
|
+
plan = status.user.plan || 'free';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Auth unavailable — default to free
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Paid plans have unlimited usage
|
|
48
|
+
if (plan !== 'free') {
|
|
49
|
+
setState({
|
|
50
|
+
used: 0,
|
|
51
|
+
limit: Infinity,
|
|
52
|
+
remaining: Infinity,
|
|
53
|
+
allowed: true,
|
|
54
|
+
plan,
|
|
55
|
+
loading: false,
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Free plan — count from local DB via API route
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch('/api/usage');
|
|
63
|
+
console.log('[usage] UsageContext fetch status:', res.status);
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
console.error('[usage] UsageContext fetch not ok:', res.status, res.statusText);
|
|
66
|
+
setState(prev => ({ ...prev, allowed: true, loading: false }));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const usage = await res.json();
|
|
70
|
+
console.log('[usage] UsageContext received:', usage);
|
|
71
|
+
setState({
|
|
72
|
+
used: typeof usage.used === 'number' ? usage.used : 0,
|
|
73
|
+
limit: typeof usage.limit === 'number' ? usage.limit : FREE_WEEKLY_LIMIT,
|
|
74
|
+
remaining: typeof usage.remaining === 'number' ? usage.remaining : FREE_WEEKLY_LIMIT,
|
|
75
|
+
allowed: typeof usage.allowed === 'boolean' ? usage.allowed : true,
|
|
76
|
+
plan,
|
|
77
|
+
loading: false,
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error('[usage] UsageContext fetch error:', err);
|
|
81
|
+
setState(prev => ({ ...prev, allowed: true, loading: false }));
|
|
82
|
+
}
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
// Fetch on mount
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
fetchUsage();
|
|
88
|
+
}, [fetchUsage]);
|
|
89
|
+
|
|
90
|
+
// Refresh on window focus
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const handleFocus = () => fetchUsage();
|
|
93
|
+
window.addEventListener('focus', handleFocus);
|
|
94
|
+
return () => window.removeEventListener('focus', handleFocus);
|
|
95
|
+
}, [fetchUsage]);
|
|
96
|
+
|
|
97
|
+
// Refresh on WebSocket db_change events (e.g., CLI creates a work item)
|
|
98
|
+
const handleWsMessage = useCallback((message: WebSocketMessage) => {
|
|
99
|
+
if (message.type === 'db_change') {
|
|
100
|
+
fetchUsage();
|
|
101
|
+
}
|
|
102
|
+
}, [fetchUsage]);
|
|
103
|
+
|
|
104
|
+
const wsUrl = typeof window !== 'undefined'
|
|
105
|
+
? `ws://${window.location.hostname}:47808`
|
|
106
|
+
: 'ws://localhost:47808';
|
|
107
|
+
|
|
108
|
+
useWebSocket({ url: wsUrl, onMessage: handleWsMessage });
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<UsageContext.Provider value={{ ...state, refresh: fetchUsage }}>
|
|
112
|
+
{children}
|
|
113
|
+
</UsageContext.Provider>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function useUsage() {
|
|
118
|
+
const context = useContext(UsageContext);
|
|
119
|
+
if (!context) {
|
|
120
|
+
return {
|
|
121
|
+
used: 0,
|
|
122
|
+
limit: FREE_WEEKLY_LIMIT,
|
|
123
|
+
remaining: FREE_WEEKLY_LIMIT,
|
|
124
|
+
allowed: true,
|
|
125
|
+
plan: 'free',
|
|
126
|
+
loading: false,
|
|
127
|
+
refresh: async () => {},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return context;
|
|
131
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
function checkUsageLimit(plan, used, limit) {
|
|
2
|
+
if (plan !== 'free') {
|
|
3
|
+
return { allowed: true, used: 0, limit: Infinity, remaining: Infinity };
|
|
4
|
+
}
|
|
5
|
+
const remaining = Math.max(0, limit - used);
|
|
6
|
+
return { allowed: remaining > 0, used, limit, remaining };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = { checkUsageLimit };
|