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
@@ -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
- narratedMode: true,
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
- // Create stream manager in registry
451
- const streamManager = getOrCreateStreamManager(id, false);
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
- // sendMessage() handles adding the user message to the stream manager internally
476
- const initialMessage = `I'm starting work on ${type || 'work item'} #${id}: ${title}`;
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
- }, [sessions, registry, getOrCreateStreamManager, ensureStreamManager, setActiveSessionId, getStateMachine]);
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
- if (!machine.canSend('SEND')) {
764
- console.warn(`[ClaudeSessionContext] Cannot send message: invalid state transition from ${machine.state}`);
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
- // Get stream manager from registry
769
- const streamManager = registry.get(currentActiveId);
770
- if (!streamManager) {
771
- // Lazy create if needed
772
- const session = sessions.get(currentActiveId);
773
- if (!session) return;
774
- const mgr = ensureStreamManager(currentActiveId, session);
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
- // Transition state machine and mark as streaming
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 };