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.
Files changed (162) hide show
  1. package/.env +7 -0
  2. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
  3. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
  4. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
  5. package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
  6. package/apps/dashboard/app/api/usage/route.ts +17 -0
  7. package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
  8. package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
  9. package/apps/dashboard/app/connect-claude/page.tsx +24 -0
  10. package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
  11. package/apps/dashboard/app/demo/gates/page.tsx +42 -42
  12. package/apps/dashboard/app/design-system/page.tsx +868 -0
  13. package/apps/dashboard/app/globals.css +6 -2
  14. package/apps/dashboard/app/install-claude/page.tsx +9 -7
  15. package/apps/dashboard/app/layout.tsx +17 -5
  16. package/apps/dashboard/app/login/page.tsx +250 -0
  17. package/apps/dashboard/app/page.tsx +11 -9
  18. package/apps/dashboard/app/settings/page.tsx +4 -2
  19. package/apps/dashboard/app/signup/page.tsx +245 -0
  20. package/apps/dashboard/app/subscribe/page.tsx +11 -0
  21. package/apps/dashboard/app/welcome/page.tsx +24 -1
  22. package/apps/dashboard/app/work/[id]/page.tsx +34 -50
  23. package/apps/dashboard/components/AppShell.tsx +95 -55
  24. package/apps/dashboard/components/CardMenu.tsx +56 -13
  25. package/apps/dashboard/components/ClaudePanel.tsx +301 -582
  26. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
  27. package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
  28. package/apps/dashboard/components/CopyableId.tsx +3 -3
  29. package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
  30. package/apps/dashboard/components/DragContext.tsx +75 -65
  31. package/apps/dashboard/components/DraggableCard.tsx +6 -46
  32. package/apps/dashboard/components/DropZone.tsx +2 -2
  33. package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
  34. package/apps/dashboard/components/EditableTitle.tsx +26 -6
  35. package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
  36. package/apps/dashboard/components/EpicGroup.tsx +329 -0
  37. package/apps/dashboard/components/GateCard.tsx +100 -16
  38. package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
  39. package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
  40. package/apps/dashboard/components/JettyLoader.tsx +38 -0
  41. package/apps/dashboard/components/KanbanBoard.tsx +147 -766
  42. package/apps/dashboard/components/KanbanCard.tsx +506 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
  44. package/apps/dashboard/components/MainNav.tsx +20 -54
  45. package/apps/dashboard/components/MessageBlock.tsx +391 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -15
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
  53. package/apps/dashboard/components/ReviewFooter.tsx +141 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -18
  55. package/apps/dashboard/components/SubscribeContent.tsx +206 -0
  56. package/apps/dashboard/components/TestTree.tsx +15 -14
  57. package/apps/dashboard/components/TipCard.tsx +177 -0
  58. package/apps/dashboard/components/Toast.tsx +5 -5
  59. package/apps/dashboard/components/TypeIcon.tsx +56 -0
  60. package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
  62. package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
  64. package/apps/dashboard/components/WorkItemTree.tsx +9 -28
  65. package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
  66. package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
  67. package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
  68. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
  69. package/apps/dashboard/components/ui/Button.tsx +104 -0
  70. package/apps/dashboard/components/ui/Input.tsx +78 -0
  71. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
  72. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
  73. package/apps/dashboard/contexts/UsageContext.tsx +155 -0
  74. package/apps/dashboard/contexts/usageHelpers.js +9 -0
  75. package/apps/dashboard/electron/ipc-handlers.js +281 -88
  76. package/apps/dashboard/electron/main.js +691 -131
  77. package/apps/dashboard/electron/preload.js +25 -4
  78. package/apps/dashboard/electron/session-manager.js +163 -0
  79. package/apps/dashboard/electron-builder.config.js +3 -5
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/lib/backlog-parser.ts +50 -0
  83. package/apps/dashboard/lib/claude-process-manager.ts +50 -11
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/db-bridge.ts +33 -0
  86. package/apps/dashboard/lib/db.ts +136 -20
  87. package/apps/dashboard/lib/kanban-utils.ts +70 -0
  88. package/apps/dashboard/lib/run-migrations.js +27 -2
  89. package/apps/dashboard/lib/session-state-machine.ts +3 -0
  90. package/apps/dashboard/lib/session-stream-manager.ts +144 -38
  91. package/apps/dashboard/lib/shadows.ts +7 -0
  92. package/apps/dashboard/lib/tests.ts +3 -1
  93. package/apps/dashboard/lib/utils.ts +6 -0
  94. package/apps/dashboard/next.config.js +35 -14
  95. package/apps/dashboard/package.json +6 -3
  96. package/apps/dashboard/public/bug-icon.svg +9 -0
  97. package/apps/dashboard/public/buoy-icon.svg +9 -0
  98. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  99. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  100. package/apps/dashboard/public/in-flight-seagull.svg +9 -0
  101. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  102. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  103. package/apps/dashboard/public/jettypod_logo.png +0 -0
  104. package/apps/dashboard/public/pier-icon.svg +14 -0
  105. package/apps/dashboard/public/star-icon.svg +9 -0
  106. package/apps/dashboard/public/wrench-icon.svg +9 -0
  107. package/apps/dashboard/scripts/upload-to-r2.js +89 -0
  108. package/apps/dashboard/scripts/ws-server.js +191 -0
  109. package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
  110. package/apps/update-server/package.json +16 -0
  111. package/apps/update-server/schema.sql +31 -0
  112. package/apps/update-server/src/index.ts +1085 -0
  113. package/apps/update-server/tsconfig.json +16 -0
  114. package/apps/update-server/wrangler.toml +35 -0
  115. package/cucumber.js +9 -3
  116. package/docs/COMMAND_REFERENCE.md +34 -0
  117. package/hooks/post-checkout +32 -75
  118. package/hooks/post-merge +111 -10
  119. package/jest.setup.js +1 -0
  120. package/jettypod.js +54 -116
  121. package/lib/chore-taxonomy.js +33 -10
  122. package/lib/database.js +36 -16
  123. package/lib/db-watcher.js +1 -1
  124. package/lib/git-hooks/pre-commit +1 -1
  125. package/lib/jettypod-backup.js +27 -4
  126. package/lib/migrations/027-plan-at-creation-column.js +33 -0
  127. package/lib/migrations/028-ready-for-review-column.js +27 -0
  128. package/lib/migrations/029-remove-autoincrement.js +307 -0
  129. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  130. package/lib/migrations/index.js +47 -4
  131. package/lib/schema.js +13 -6
  132. package/lib/seed-onboarding.js +101 -69
  133. package/lib/update-command/index.js +9 -175
  134. package/lib/work-commands/index.js +129 -16
  135. package/lib/work-tracking/index.js +86 -46
  136. package/lib/worktree-diagnostics.js +16 -16
  137. package/lib/worktree-facade.js +1 -1
  138. package/lib/worktree-manager.js +8 -8
  139. package/lib/worktree-reconciler.js +5 -5
  140. package/package.json +9 -2
  141. package/scripts/ndjson-to-cucumber-json.js +152 -0
  142. package/scripts/postinstall.js +25 -0
  143. package/skills-templates/bug-mode/SKILL.md +39 -28
  144. package/skills-templates/bug-planning/SKILL.md +25 -29
  145. package/skills-templates/chore-mode/SKILL.md +131 -68
  146. package/skills-templates/chore-mode/verification.js +51 -10
  147. package/skills-templates/chore-planning/SKILL.md +47 -18
  148. package/skills-templates/epic-planning/SKILL.md +68 -48
  149. package/skills-templates/external-transition/SKILL.md +47 -47
  150. package/skills-templates/feature-planning/SKILL.md +83 -73
  151. package/skills-templates/production-mode/SKILL.md +49 -49
  152. package/skills-templates/request-routing/SKILL.md +27 -14
  153. package/skills-templates/simple-improvement/SKILL.md +68 -44
  154. package/skills-templates/speed-mode/SKILL.md +209 -128
  155. package/skills-templates/stable-mode/SKILL.md +105 -94
  156. package/templates/bdd-guidance.md +139 -0
  157. package/templates/bdd-scaffolding/wait.js +18 -0
  158. package/templates/bdd-scaffolding/world.js +19 -0
  159. package/.jettypod-backup/work.db +0 -0
  160. package/apps/dashboard/app/access-code/page.tsx +0 -110
  161. package/lib/discovery-checkpoint.js +0 -123
  162. package/skills-templates/project-discovery/SKILL.md +0 -372
@@ -1,11 +1,13 @@
1
1
  'use client';
2
2
 
3
- import { createContext, useContext, useState, useCallback, useEffect, useRef, type ReactNode } from 'react';
3
+ import { createContext, useContext, useState, useCallback, useEffect, useRef, useMemo, type ReactNode } from 'react';
4
4
  import {
5
5
  type ClaudeMessage,
6
6
  type StreamStatus,
7
7
  type StreamState,
8
+ type QueuedMessage,
8
9
  } from '../lib/session-stream-manager';
10
+ import type { AttachedImage } from '../components/ClaudePanelInput';
9
11
  import { getRegistry } from '../lib/stream-manager-registry';
10
12
  import {
11
13
  createSessionRefs,
@@ -21,6 +23,7 @@ import {
21
23
  } from '../lib/session-state-machine';
22
24
  import { getMessageBuffer } from '../lib/message-buffer';
23
25
  import { useToast } from '../components/Toast';
26
+ import { useUsage } from './UsageContext';
24
27
  import type { SessionItem } from '../components/SessionList';
25
28
 
26
29
  // Re-export ClaudeMessage for consumers
@@ -39,67 +42,89 @@ export interface Session {
39
42
  narratedMode: boolean;
40
43
  }
41
44
 
42
- interface ClaudeSessionContextValue {
43
- // Panel state
44
- claudePanelOpen: boolean;
45
- setClaudePanelOpen: (open: boolean) => void;
45
+ // Split into 3 contexts to prevent unnecessary re-renders:
46
+ // - SessionStateContext: frequently-changing state (messages, status, sessions)
47
+ // - SessionActionsContext: stable callbacks (sendMessage, switchSession, etc.)
48
+ // - SessionPersistenceContext: internal mutation setters (setSessions, etc.)
46
49
 
47
- // Session state
50
+ interface SessionStateContextValue {
51
+ claudePanelOpen: boolean;
48
52
  sessions: Map<string, Session>;
49
53
  activeSessionId: string | null;
50
54
  activeSession: Session | null;
51
-
52
- // Standalone sessions
53
55
  standaloneSessions: SessionItem[];
54
-
55
- // Stream state (from active session's stream manager)
56
56
  messages: ClaudeMessage[];
57
57
  status: StreamStatus;
58
58
  error: string | null;
59
59
  exitCode: number | null;
60
60
  canRetry: boolean;
61
+ queuedMessage: QueuedMessage | null;
61
62
  narratedMode: boolean;
62
- toggleNarratedMode: () => void;
63
+ isTabSwitching: boolean;
64
+ }
63
65
 
64
- // Session actions
65
- openSession: (id: string, title: string, type?: string) => void;
66
+ interface SessionActionsContextValue {
67
+ setClaudePanelOpen: (open: boolean) => void;
68
+ openSession: (id: string, title: string, type?: string, conversational?: boolean, description?: string | null, initialHidden?: boolean) => 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>;
72
-
73
- // Stream actions
74
- sendMessage: (message: string) => void;
75
+ createFixScenarioSession: (featureFile: string, scenarioTitle: string, error: string, failedStep?: string, steps?: string[]) => Promise<void>;
76
+ createWelcomeSession: () => Promise<void>;
77
+ sendMessage: (message: string, images?: AttachedImage[]) => void;
75
78
  retry: () => void;
76
79
  stop: () => void;
80
+ toggleNarratedMode: () => void;
81
+ }
77
82
 
78
- // For components that need to set messages directly (e.g., restoring from DB)
83
+ interface SessionPersistenceContextValue {
79
84
  setMessages: (messages: ClaudeMessage[]) => void;
80
85
  setStatus: (status: StreamStatus) => void;
81
-
82
- // For RealTimeKanbanWrapper to update sessions
83
86
  setSessions: React.Dispatch<React.SetStateAction<Map<string, Session>>>;
84
87
  setActiveSessionId: (id: string | null) => void;
85
88
  setStandaloneSessions: React.Dispatch<React.SetStateAction<SessionItem[]>>;
86
89
  }
87
90
 
88
- const ClaudeSessionContext = createContext<ClaudeSessionContextValue | null>(null);
91
+ const SessionStateContext = createContext<SessionStateContextValue | null>(null);
92
+ const SessionActionsContext = createContext<SessionActionsContextValue | null>(null);
93
+ const SessionPersistenceContext = createContext<SessionPersistenceContextValue | null>(null);
89
94
 
90
- export function useClaudeSession() {
91
- const context = useContext(ClaudeSessionContext);
92
- if (!context) {
93
- throw new Error('useClaudeSession must be used within a ClaudeSessionProvider');
94
- }
95
+ // Granular hooks — use these for better performance
96
+ export function useSessionState() {
97
+ const context = useContext(SessionStateContext);
98
+ if (!context) throw new Error('useSessionState must be used within a ClaudeSessionProvider');
95
99
  return context;
96
100
  }
97
101
 
102
+ export function useSessionActions() {
103
+ const context = useContext(SessionActionsContext);
104
+ if (!context) throw new Error('useSessionActions must be used within a ClaudeSessionProvider');
105
+ return context;
106
+ }
107
+
108
+ export function useSessionPersistence() {
109
+ const context = useContext(SessionPersistenceContext);
110
+ if (!context) throw new Error('useSessionPersistence must be used within a ClaudeSessionProvider');
111
+ return context;
112
+ }
113
+
114
+ // Backward-compatible hook — combines all 3 contexts
115
+ export function useClaudeSession() {
116
+ const state = useSessionState();
117
+ const actions = useSessionActions();
118
+ const persistence = useSessionPersistence();
119
+ return { ...state, ...actions, ...persistence };
120
+ }
121
+
98
122
  const ACTIVE_SESSION_KEY = 'jettypod-active-session-id';
99
123
 
100
124
  export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
101
125
  // Toast for user notifications
102
126
  const { showToast } = useToast();
127
+ const { allowed: usageAllowed, refresh: refreshUsage } = useUsage();
103
128
 
104
129
  // Get registry singleton (stable across renders)
105
130
  const registry = getRegistry();
@@ -172,6 +197,9 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
172
197
  // AbortController for cancelling in-flight historical content fetches (#1000101)
173
198
  const contentFetchAbortRef = useRef<AbortController | null>(null);
174
199
 
200
+ // Reactive tab-switching state (exposed to UI so components can suppress flash of empty state)
201
+ const [isTabSwitching, setIsTabSwitching] = useState(false);
202
+
175
203
  // Force re-render when stream state changes (since stream managers are mutable)
176
204
  const [, forceUpdate] = useState({});
177
205
 
@@ -193,6 +221,17 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
193
221
  machine.send('CONNECTED');
194
222
  } else if (state.status === 'done') {
195
223
  machine.send('COMPLETE');
224
+ // Auto-send queued message after stream completes
225
+ if (state.queuedMessage) {
226
+ const streamManager = registry.get(sessionId);
227
+ if (streamManager && machine.canSend('SEND')) {
228
+ const { message, images } = state.queuedMessage;
229
+ streamManager.clearQueuedMessage();
230
+ machine.send('SEND');
231
+ setSessionStreaming(sessionRefs, sessionId, true);
232
+ streamManager.sendMessage(message, images);
233
+ }
234
+ }
196
235
  } else if (state.status === 'error') {
197
236
  machine.send('ERROR');
198
237
  } else if (state.status === 'idle' && machine.state !== 'idle') {
@@ -253,11 +292,12 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
253
292
  const getOrCreateStreamManager = useCallback((
254
293
  sessionId: string,
255
294
  standalone: boolean,
256
- onWorkItemCreated?: (workItemId: number, title: string) => void
295
+ onWorkItemCreated?: (workItemId: number, title: string) => void,
296
+ conversational?: boolean
257
297
  ) => {
258
298
  // Registry handles idempotent creation and state change events
259
299
  return registry.create(sessionId, {
260
- context: { workItemId: sessionId, standalone },
300
+ context: { workItemId: sessionId, standalone, conversational },
261
301
  callbacks: { onWorkItemCreated },
262
302
  });
263
303
  }, [registry]);
@@ -266,26 +306,29 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
266
306
  // Note: The stream manager already guards on sessionContext.standalone before
267
307
  // firing this callback, so we don't need to re-check sessions here (which
268
308
  // would fail due to stale closure over the sessions Map).
269
- const handleWorkItemCreated = useCallback(async (workItemId: number, title: string) => {
270
- // Use sync ref to get current active session (avoids stale closure)
271
- const currentActiveId = sessionRefs.activeSessionId.current;
272
- if (!currentActiveId) return;
309
+ const handleWorkItemCreated = useCallback(async (workItemId: number, title: string, sourceSessionId: string) => {
310
+ // Refresh usage count from local DB (work item now exists in work.db)
311
+ refreshUsage();
312
+
313
+ // Use the source session ID passed by the stream manager — NOT activeSessionId,
314
+ // which may point to a different tab if the user switched during streaming (#1001272)
315
+ if (!sourceSessionId) return;
273
316
 
274
317
  try {
275
318
  const response = await fetch('/api/claude/sessions', {
276
319
  method: 'PATCH',
277
320
  headers: { 'Content-Type': 'application/json' },
278
321
  body: JSON.stringify({
279
- sessionId: parseInt(currentActiveId, 10),
322
+ sessionId: parseInt(sourceSessionId, 10),
280
323
  workItemId,
281
324
  }),
282
325
  });
283
326
 
284
- if (response.ok) {
327
+ if (response.ok || response.status === 409) {
285
328
  const newSessionId = String(workItemId);
286
329
 
287
330
  // Update stream manager's session context via registry
288
- const streamManager = registry.get(currentActiveId);
331
+ const streamManager = registry.get(sourceSessionId);
289
332
  if (streamManager) {
290
333
  streamManager.updateSessionContext({
291
334
  workItemId: newSessionId,
@@ -295,13 +338,13 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
295
338
 
296
339
  // Atomic session update (#1000102): Build new map in one pass
297
340
  setSessions(prev => {
298
- const session = prev.get(currentActiveId);
341
+ const session = prev.get(sourceSessionId);
299
342
  if (!session) return prev;
300
343
 
301
344
  // Build new map: copy all except old session, add new session
302
345
  const updated = new Map<string, Session>();
303
346
  for (const [key, value] of prev) {
304
- if (key !== currentActiveId) {
347
+ if (key !== sourceSessionId) {
305
348
  updated.set(key, value);
306
349
  }
307
350
  }
@@ -319,13 +362,13 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
319
362
 
320
363
  // Remove from standalone sessions list
321
364
  setStandaloneSessions(prev =>
322
- prev.filter(s => s.id !== currentActiveId)
365
+ prev.filter(s => s.id !== sourceSessionId)
323
366
  );
324
367
  }
325
368
  } catch (err) {
326
369
  console.error('Failed to link session to work item:', err);
327
370
  }
328
- }, [sessionRefs, registry, setActiveSessionId]);
371
+ }, [registry, setActiveSessionId, refreshUsage]);
329
372
 
330
373
  // Load persisted sessions from backend on mount
331
374
  useEffect(() => {
@@ -397,7 +440,8 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
397
440
  status: 'idle',
398
441
  error: null,
399
442
  exitCode: null,
400
- narratedMode: true,
443
+ // Welcome session shows static content — detail view shows all messages
444
+ narratedMode: session.title !== 'Welcome',
401
445
  // Stream manager created lazily when session becomes active
402
446
  });
403
447
  }
@@ -436,7 +480,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
436
480
  // Open or create a session for a work item
437
481
  // With per-session streams, no need to stop other streams or track ownership
438
482
  // Auto-sends an initial message so Claude starts working immediately
439
- const openSession = useCallback((id: string, title: string, type?: string) => {
483
+ const openSession = useCallback((id: string, title: string, type?: string, conversational?: boolean, description?: string | null, initialHidden?: boolean) => {
440
484
  if (sessions.has(id)) {
441
485
  // Switching to existing session - ensure it has a stream manager
442
486
  const session = sessions.get(id)!;
@@ -447,8 +491,13 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
447
491
  return;
448
492
  }
449
493
 
450
- // Create stream manager in registry
451
- const streamManager = getOrCreateStreamManager(id, false);
494
+ // Gate new session creation on usage limits
495
+ if (!usageAllowed) {
496
+ return;
497
+ }
498
+
499
+ // Create stream manager in registry — pass conversational flag for skip-delay behavior
500
+ const streamManager = getOrCreateStreamManager(id, false, undefined, conversational);
452
501
  registry.acquire(id);
453
502
 
454
503
  const newSession: Session = {
@@ -472,12 +521,18 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
472
521
  setClaudePanelOpen(true);
473
522
 
474
523
  // 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}`;
524
+ // Conversational chores: send description as hidden message so Claude speaks first
525
+ // Non-conversational: send visible "starting work" message
526
+ const initialMessage = conversational
527
+ ? `This is a conversation — no code, no worktrees. Just chat with me naturally.\n\n${description || title}`
528
+ : `I'm starting work on ${type || 'work item'} #${id}: ${title}`;
477
529
  const machine = getStateMachine(id);
478
530
  machine.send('SEND');
479
- streamManager.sendMessage(initialMessage);
480
- }, [sessions, registry, getOrCreateStreamManager, ensureStreamManager, setActiveSessionId, getStateMachine]);
531
+ streamManager.sendMessage(initialMessage, undefined, (conversational || initialHidden) ? { hidden: true } : undefined);
532
+
533
+ // Refresh usage so UI reflects the new session immediately
534
+ refreshUsage();
535
+ }, [sessions, registry, getOrCreateStreamManager, ensureStreamManager, setActiveSessionId, getStateMachine, usageAllowed, showToast, refreshUsage]);
481
536
 
482
537
  // Track target session during async switch operations (for tab-switching UX, not streaming)
483
538
  const switchingToRef = useRef<string | null>(null);
@@ -488,8 +543,9 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
488
543
  const switchSession = useCallback(async (id: string) => {
489
544
  if (!sessions.has(id)) return;
490
545
 
491
- // Mark tab switch in progress (sync ref for callbacks)
546
+ // Mark tab switch in progress (sync ref for callbacks + reactive state for UI)
492
547
  sessionRefs.isTabSwitching.set(true);
548
+ setIsTabSwitching(true);
493
549
 
494
550
  // Start buffering messages for the previous session to prevent loss
495
551
  const previousSessionId = sessionRefs.activeSessionId.current;
@@ -612,36 +668,24 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
612
668
  switchingToRef.current = null;
613
669
  }
614
670
  sessionRefs.isTabSwitching.set(false);
671
+ setIsTabSwitching(false);
615
672
  }, 100); // 100ms debounce delay
616
673
  }, [sessions, sessionRefs, registry, ensureStreamManager, setActiveSessionId, messageBuffer]);
617
674
 
618
675
  // Close a session
619
676
  // With per-session streams, we delete the stream manager from registry when closing
620
- const closeSession = useCallback(async (sessionId: string) => {
677
+ const closeSession = useCallback((sessionId: string) => {
621
678
  // Get session to determine API type
622
679
  const session = sessions.get(sessionId);
623
680
  const sessionType = session?.type || 'standalone';
624
681
 
625
- // Unpin the session being closed
626
- const endpoint = sessionType === 'workitem'
627
- ? `/api/claude/${sessionId}/pin`
628
- : `/api/claude/sessions/${sessionId}/pin`;
629
- fetch(endpoint, { method: 'DELETE' }).catch(() => {});
630
-
631
682
  // Delete stream manager from registry (handles destroy + cleanup)
632
683
  registry.delete(sessionId);
633
684
 
634
685
  // Clear streaming status in sync ref
635
686
  setSessionStreaming(sessionRefs, sessionId, false);
636
687
 
637
- try {
638
- await fetch(`/api/claude/sessions?sessionId=${sessionId}&type=${sessionType}`, {
639
- method: 'DELETE',
640
- });
641
- } catch (err) {
642
- console.error('[ClaudeSessionContext] Failed to close session:', err);
643
- }
644
-
688
+ // Update UI state immediately (before any async work)
645
689
  setStandaloneSessions(prev => prev.filter(s => s.id !== sessionId));
646
690
  setSessions(prev => {
647
691
  const updated = new Map(prev);
@@ -665,11 +709,26 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
665
709
  setActiveSessionId(remaining[Math.min(newIndex, remaining.length - 1)]);
666
710
  }
667
711
  }
712
+
713
+ // API cleanup in background (unpin + delete) — UI already updated above
714
+ const endpoint = sessionType === 'workitem'
715
+ ? `/api/claude/${sessionId}/pin`
716
+ : `/api/claude/sessions/${sessionId}/pin`;
717
+ fetch(endpoint, { method: 'DELETE' }).catch(() => {});
718
+ fetch(`/api/claude/sessions?sessionId=${sessionId}&type=${sessionType}`, {
719
+ method: 'DELETE',
720
+ }).catch(err => {
721
+ console.error('[ClaudeSessionContext] Failed to close session:', err);
722
+ });
668
723
  }, [activeSessionId, sessions, standaloneSessions, registry, sessionRefs, setActiveSessionId]);
669
724
 
670
725
  // Create a new standalone session
671
726
  // With per-session streams, each new session gets its own stream manager in registry
672
727
  const createNewSession = useCallback(async () => {
728
+ if (!usageAllowed) {
729
+ return;
730
+ }
731
+
673
732
  try {
674
733
  const response = await fetch('/api/claude/sessions', {
675
734
  method: 'POST',
@@ -721,10 +780,13 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
721
780
 
722
781
  setActiveSessionId(id);
723
782
  setClaudePanelOpen(true);
783
+
784
+ // Refresh usage so UI reflects the new session immediately
785
+ refreshUsage();
724
786
  } catch (err) {
725
787
  console.error('[ClaudeSessionContext] Failed to create session:', err);
726
788
  }
727
- }, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId]);
789
+ }, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, usageAllowed, refreshUsage]);
728
790
 
729
791
  // Open the session panel (restore last session or create new)
730
792
  const openSessionPanel = useCallback(() => {
@@ -743,14 +805,16 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
743
805
  return;
744
806
  }
745
807
 
746
- // No sessions exist - create new standalone session
747
- createNewSession();
808
+ // No sessions exist - create new standalone session (unless over usage limit)
809
+ if (usageAllowed) {
810
+ createNewSession();
811
+ }
748
812
  setClaudePanelOpen(true);
749
- }, [sessions, switchSession, createNewSession]);
813
+ }, [sessions, switchSession, createNewSession, usageAllowed]);
750
814
 
751
815
  // Send message via the active session's stream manager
752
816
  // With per-session streams, each session has its own manager - no cross-contamination possible
753
- const sendMessage = useCallback((message: string) => {
817
+ const sendMessage = useCallback((message: string, images?: AttachedImage[]) => {
754
818
  // Use sync ref for current active session (avoids stale closure)
755
819
  const currentActiveId = sessionRefs.activeSessionId.current;
756
820
  if (!currentActiveId) {
@@ -758,29 +822,38 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
758
822
  return;
759
823
  }
760
824
 
761
- // Validate state transition via state machine
762
825
  const machine = getStateMachine(currentActiveId);
763
- if (!machine.canSend('SEND')) {
764
- console.warn(`[ClaudeSessionContext] Cannot send message: invalid state transition from ${machine.state}`);
826
+
827
+ // If we can send directly, do so
828
+ if (machine.canSend('SEND')) {
829
+ const streamManager = registry.get(currentActiveId);
830
+ if (!streamManager) {
831
+ // Lazy create if needed
832
+ const session = sessions.get(currentActiveId);
833
+ if (!session) return;
834
+ const mgr = ensureStreamManager(currentActiveId, session);
835
+ machine.send('SEND');
836
+ mgr.sendMessage(message, images);
837
+ return;
838
+ }
839
+
840
+ machine.send('SEND');
841
+ setSessionStreaming(sessionRefs, currentActiveId, true);
842
+ streamManager.sendMessage(message, images);
765
843
  return;
766
844
  }
767
845
 
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);
846
+ // If streaming/connecting, queue the message for later
847
+ if (machine.canSend('QUEUE')) {
848
+ const streamManager = registry.get(currentActiveId);
849
+ if (!streamManager) return;
850
+
851
+ machine.send('QUEUE');
852
+ streamManager.queueMessage(message, images);
777
853
  return;
778
854
  }
779
855
 
780
- // Transition state machine and mark as streaming
781
- machine.send('SEND');
782
- setSessionStreaming(sessionRefs, currentActiveId, true);
783
- streamManager.sendMessage(message);
856
+ console.warn(`[ClaudeSessionContext] Cannot send message: invalid state transition from ${machine.state}`);
784
857
  }, [sessionRefs, registry, sessions, ensureStreamManager, getStateMachine]);
785
858
 
786
859
  // Retry via the active session's stream manager
@@ -825,20 +898,41 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
825
898
  setSessionStreaming(sessionRefs, currentActiveId, false);
826
899
  }, [sessionRefs, registry, getStateMachine]);
827
900
 
828
- // Toggle narrated mode for the active session's stream manager
901
+ // Toggle narrated mode directly in React state, keeping stream manager in sync
902
+ // without triggering notifyStateChange (which would overwrite React state with
903
+ // potentially stale stream manager state — the root cause of the blank chat bug).
829
904
  const toggleNarratedMode = useCallback(() => {
830
905
  const currentActiveId = sessionRefs.activeSessionId.current;
831
906
  if (!currentActiveId) return;
832
907
 
833
- const streamManager = registry.get(currentActiveId);
834
- if (!streamManager) return;
908
+ setSessions(prev => {
909
+ const session = prev.get(currentActiveId);
910
+ if (!session) return prev;
835
911
 
836
- streamManager.setNarratedMode(!streamManager.narratedMode);
912
+ const newNarratedMode = !session.narratedMode;
913
+
914
+ // Keep stream manager in sync (quiet — no notifyStateChange)
915
+ const streamManager = registry.get(currentActiveId);
916
+ if (streamManager) {
917
+ streamManager.setNarratedModeQuiet(newNarratedMode);
918
+ }
919
+
920
+ const updated = new Map(prev);
921
+ updated.set(currentActiveId, {
922
+ ...session,
923
+ narratedMode: newNarratedMode,
924
+ });
925
+ return updated;
926
+ });
837
927
  }, [sessionRefs, registry]);
838
928
 
839
929
  // Create an "Add to Backlog" session with initial assistant message
840
930
  // With per-session streams, each session has its own manager in registry
841
931
  const createAddToBacklogSession = useCallback(async () => {
932
+ if (!usageAllowed) {
933
+ return;
934
+ }
935
+
842
936
  try {
843
937
  const response = await fetch('/api/claude/sessions', {
844
938
  method: 'POST',
@@ -870,7 +964,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
870
964
  // Create initial assistant message
871
965
  const initialMessage: ClaudeMessage = {
872
966
  type: 'assistant',
873
- content: 'What should we add to the backlog?',
967
+ content: 'What should the name of this work item be?',
874
968
  timestamp: Date.now(),
875
969
  };
876
970
 
@@ -899,13 +993,115 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
899
993
 
900
994
  setActiveSessionId(id);
901
995
  setClaudePanelOpen(true);
996
+
997
+ // Refresh usage so UI reflects the new session immediately
998
+ refreshUsage();
902
999
  } catch (err) {
903
1000
  console.error('[ClaudeSessionContext] Failed to create backlog session:', err);
904
1001
  }
1002
+ }, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, usageAllowed, refreshUsage]);
1003
+
1004
+ // Create a welcome session for blank projects with initial assistant message
1005
+ const createWelcomeSession = useCallback(async () => {
1006
+ try {
1007
+ const response = await fetch('/api/claude/sessions', {
1008
+ method: 'POST',
1009
+ headers: { 'Content-Type': 'application/json' },
1010
+ body: JSON.stringify({ title: 'Welcome' }),
1011
+ });
1012
+
1013
+ if (!response.ok) {
1014
+ const errorData = await response.json();
1015
+ if (errorData.code === 'SESSION_LIMIT_REACHED') {
1016
+ console.warn(`[ClaudeSessionContext] ${errorData.error}`);
1017
+ showToast('Session limit reached. Close existing sessions to create new ones.', 'error');
1018
+ }
1019
+ return;
1020
+ }
1021
+
1022
+ const { id, title } = await response.json();
1023
+
1024
+ setStandaloneSessions(prev => [...prev, {
1025
+ id,
1026
+ title,
1027
+ featureId: null,
1028
+ featureTitle: null,
1029
+ }]);
1030
+
1031
+ const now = Date.now();
1032
+
1033
+ const greetingMessage: ClaudeMessage = {
1034
+ type: 'assistant',
1035
+ content: 'Ahoy. Your project is set up and ready to go.',
1036
+ timestamp: now,
1037
+ };
1038
+
1039
+ const backlogTip: ClaudeMessage = {
1040
+ type: 'gate',
1041
+ gateType: 'tip',
1042
+ gateData: {
1043
+ id: 'tip-backlog',
1044
+ icon: '📋',
1045
+ title: 'The Backlog',
1046
+ 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',
1047
+ },
1048
+ timestamp: now + 1,
1049
+ };
1050
+
1051
+ const workflowTip: ClaudeMessage = {
1052
+ type: 'gate',
1053
+ gateType: 'tip',
1054
+ gateData: {
1055
+ id: 'tip-workflow',
1056
+ icon: '🔄',
1057
+ title: 'How to Work',
1058
+ 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',
1059
+ },
1060
+ timestamp: now + 2,
1061
+ };
1062
+
1063
+ const ctaMessage: ClaudeMessage = {
1064
+ type: 'assistant',
1065
+ content: 'Close this welcome chat and start your first onboarding chore\u2014**Align on the user journey**.',
1066
+ timestamp: now + 3,
1067
+ };
1068
+
1069
+ const welcomeMessages = [greetingMessage, backlogTip, workflowTip, ctaMessage];
1070
+
1071
+ const streamManager = getOrCreateStreamManager(id, true, handleWorkItemCreated);
1072
+ registry.acquire(id);
1073
+ streamManager.setMessages(welcomeMessages);
1074
+
1075
+ const newSession: Session = {
1076
+ id,
1077
+ title,
1078
+ type: 'standalone',
1079
+ messages: welcomeMessages,
1080
+ status: 'idle',
1081
+ error: null,
1082
+ exitCode: null,
1083
+ narratedMode: false,
1084
+ };
1085
+
1086
+ setSessions(prev => {
1087
+ const updated = new Map(prev);
1088
+ updated.set(id, newSession);
1089
+ return updated;
1090
+ });
1091
+
1092
+ setActiveSessionId(id);
1093
+ setClaudePanelOpen(true);
1094
+ } catch (err) {
1095
+ console.error('[ClaudeSessionContext] Failed to create welcome session:', err);
1096
+ }
905
1097
  }, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId]);
906
1098
 
907
1099
  // Create a "Run Scenario" session with preloaded cucumber-js command
908
1100
  const createRunScenarioSession = useCallback(async (featureFile: string, scenarioTitle: string) => {
1101
+ if (!usageAllowed) {
1102
+ return;
1103
+ }
1104
+
909
1105
  try {
910
1106
  const sessionTitle = `Run: ${scenarioTitle}`;
911
1107
  const response = await fetch('/api/claude/sessions', {
@@ -969,11 +1165,112 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
969
1165
  const machine = getStateMachine(id);
970
1166
  machine.send('SEND');
971
1167
  streamManager.sendMessage(userMessage.content!);
1168
+
1169
+ // Refresh usage so UI reflects the new session immediately
1170
+ refreshUsage();
972
1171
  } catch (err) {
973
1172
  console.error('[ClaudeSessionContext] Failed to create run scenario session:', err);
974
1173
  showToast('Failed to create session. Please try again.', 'error');
975
1174
  }
976
- }, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, getStateMachine]);
1175
+ }, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, getStateMachine, usageAllowed, refreshUsage]);
1176
+
1177
+ // Create a "Fix Scenario" session with preloaded failure context
1178
+ const createFixScenarioSession = useCallback(async (featureFile: string, scenarioTitle: string, error: string, failedStep?: string, steps?: string[]) => {
1179
+ if (!usageAllowed) {
1180
+ return;
1181
+ }
1182
+
1183
+ try {
1184
+ const sessionTitle = `Fix: ${scenarioTitle}`;
1185
+ const response = await fetch('/api/claude/sessions', {
1186
+ method: 'POST',
1187
+ headers: { 'Content-Type': 'application/json' },
1188
+ body: JSON.stringify({ title: sessionTitle }),
1189
+ });
1190
+
1191
+ if (!response.ok) {
1192
+ const errorData = await response.json();
1193
+ if (errorData.code === 'SESSION_LIMIT_REACHED') {
1194
+ console.warn(`[ClaudeSessionContext] ${errorData.error}`);
1195
+ showToast('Session limit reached. Close existing sessions to create new ones.', 'error');
1196
+ } else {
1197
+ console.error('[ClaudeSessionContext] Failed to create fix scenario session:', errorData.error);
1198
+ showToast('Failed to create session. Please try again.', 'error');
1199
+ }
1200
+ return;
1201
+ }
1202
+
1203
+ const { id, title } = await response.json();
1204
+
1205
+ setStandaloneSessions(prev => [...prev, {
1206
+ id,
1207
+ title,
1208
+ featureId: null,
1209
+ featureTitle: null,
1210
+ }]);
1211
+
1212
+ const promptParts = [
1213
+ 'A BDD scenario is failing and needs to be fixed.',
1214
+ '',
1215
+ `**Scenario:** ${scenarioTitle}`,
1216
+ `**Feature file:** ${featureFile}`,
1217
+ ];
1218
+
1219
+ if (failedStep) {
1220
+ promptParts.push(`**Failed step:** ${failedStep}`);
1221
+ }
1222
+
1223
+ promptParts.push('**Error:**', '```', error, '```');
1224
+
1225
+ if (steps && steps.length > 0) {
1226
+ promptParts.push('', '**Scenario steps:**');
1227
+ steps.forEach(step => promptParts.push(` ${step}`));
1228
+ }
1229
+
1230
+ promptParts.push('', 'Please investigate this failure, identify the root cause, and fix it.');
1231
+
1232
+ const userMessage: ClaudeMessage = {
1233
+ type: 'user',
1234
+ content: promptParts.join('\n'),
1235
+ timestamp: Date.now(),
1236
+ };
1237
+
1238
+ const streamManager = getOrCreateStreamManager(id, true, handleWorkItemCreated);
1239
+ registry.acquire(id);
1240
+ streamManager.setMessages([userMessage]);
1241
+
1242
+ const newSession: Session = {
1243
+ id,
1244
+ title,
1245
+ type: 'standalone',
1246
+ messages: [userMessage],
1247
+ status: 'idle',
1248
+ error: null,
1249
+ exitCode: null,
1250
+ narratedMode: true,
1251
+ };
1252
+
1253
+ setSessions(prev => {
1254
+ const updated = new Map(prev);
1255
+ updated.set(id, newSession);
1256
+ return updated;
1257
+ });
1258
+
1259
+ setActiveSessionId(id);
1260
+ setClaudePanelOpen(true);
1261
+
1262
+ // Auto-send the fix request to Claude
1263
+ const machine = getStateMachine(id);
1264
+ machine.send('SEND');
1265
+ streamManager.sendMessage(userMessage.content!);
1266
+
1267
+ // Refresh usage so UI reflects the new session immediately
1268
+ refreshUsage();
1269
+ } catch (err) {
1270
+ console.error('[ClaudeSessionContext] Failed to create fix scenario session:', err);
1271
+ showToast('Failed to create session. Please try again.', 'error');
1272
+ }
1273
+ }, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, getStateMachine, usageAllowed, refreshUsage]);
977
1274
 
978
1275
  // Setters for direct manipulation (e.g., restoring from DB)
979
1276
  // These now work through the registry
@@ -1001,27 +1298,26 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
1001
1298
  // Get canRetry from registry
1002
1299
  const activeStreamManager = activeSessionId ? registry.get(activeSessionId) : null;
1003
1300
 
1004
- const value: ClaudeSessionContextValue = {
1005
- // Panel state
1301
+ // Memoize state context — changes when state values change
1302
+ const stateValue: SessionStateContextValue = useMemo(() => ({
1006
1303
  claudePanelOpen,
1007
- setClaudePanelOpen,
1008
-
1009
- // Session state
1010
1304
  sessions,
1011
1305
  activeSessionId,
1012
1306
  activeSession,
1013
1307
  standaloneSessions,
1014
-
1015
- // Stream state (from active session)
1016
1308
  messages: activeSession?.messages ?? [],
1017
1309
  status: activeSession?.status ?? 'idle',
1018
1310
  error: activeSession?.error ?? null,
1019
1311
  exitCode: activeSession?.exitCode ?? null,
1020
1312
  canRetry: activeStreamManager?.canRetry ?? false,
1313
+ queuedMessage: activeStreamManager?.queuedMessage ?? null,
1021
1314
  narratedMode: activeSession?.narratedMode ?? false,
1022
- toggleNarratedMode,
1315
+ isTabSwitching,
1316
+ }), [claudePanelOpen, sessions, activeSessionId, activeSession, standaloneSessions, activeStreamManager, isTabSwitching]);
1023
1317
 
1024
- // Session actions
1318
+ // Memoize actions context — stable callbacks, rarely changes
1319
+ const actionsValue: SessionActionsContextValue = useMemo(() => ({
1320
+ setClaudePanelOpen,
1025
1321
  openSession,
1026
1322
  switchSession,
1027
1323
  closeSession,
@@ -1029,23 +1325,30 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
1029
1325
  createNewSession,
1030
1326
  createAddToBacklogSession,
1031
1327
  createRunScenarioSession,
1032
-
1033
- // Stream actions (now go through registry)
1328
+ createFixScenarioSession,
1329
+ createWelcomeSession,
1034
1330
  sendMessage,
1035
1331
  retry,
1036
1332
  stop,
1333
+ toggleNarratedMode,
1334
+ }), [setClaudePanelOpen, openSession, switchSession, closeSession, openSessionPanel, createNewSession, createAddToBacklogSession, createRunScenarioSession, createFixScenarioSession, createWelcomeSession, sendMessage, retry, stop, toggleNarratedMode]);
1037
1335
 
1038
- // Direct setters (work through registry)
1336
+ // Memoize persistence context stable setters
1337
+ const persistenceValue: SessionPersistenceContextValue = useMemo(() => ({
1039
1338
  setMessages,
1040
1339
  setStatus,
1041
1340
  setSessions,
1042
1341
  setActiveSessionId,
1043
1342
  setStandaloneSessions,
1044
- };
1343
+ }), [setMessages, setStatus, setSessions, setActiveSessionId, setStandaloneSessions]);
1045
1344
 
1046
1345
  return (
1047
- <ClaudeSessionContext.Provider value={value}>
1048
- {children}
1049
- </ClaudeSessionContext.Provider>
1346
+ <SessionStateContext.Provider value={stateValue}>
1347
+ <SessionActionsContext.Provider value={actionsValue}>
1348
+ <SessionPersistenceContext.Provider value={persistenceValue}>
1349
+ {children}
1350
+ </SessionPersistenceContext.Provider>
1351
+ </SessionActionsContext.Provider>
1352
+ </SessionStateContext.Provider>
1050
1353
  );
1051
1354
  }