jettypod 4.4.116 → 4.4.120
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +7 -0
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
- package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
- package/apps/dashboard/app/api/usage/route.ts +17 -0
- package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
- package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
- package/apps/dashboard/app/connect-claude/page.tsx +24 -0
- package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
- package/apps/dashboard/app/demo/gates/page.tsx +42 -42
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +6 -2
- package/apps/dashboard/app/install-claude/page.tsx +9 -7
- package/apps/dashboard/app/layout.tsx +17 -5
- package/apps/dashboard/app/login/page.tsx +250 -0
- package/apps/dashboard/app/page.tsx +11 -9
- package/apps/dashboard/app/settings/page.tsx +4 -2
- package/apps/dashboard/app/signup/page.tsx +245 -0
- package/apps/dashboard/app/subscribe/page.tsx +11 -0
- package/apps/dashboard/app/welcome/page.tsx +24 -1
- package/apps/dashboard/app/work/[id]/page.tsx +34 -50
- package/apps/dashboard/components/AppShell.tsx +95 -55
- package/apps/dashboard/components/CardMenu.tsx +56 -13
- package/apps/dashboard/components/ClaudePanel.tsx +301 -582
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
- package/apps/dashboard/components/CopyableId.tsx +3 -3
- package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
- package/apps/dashboard/components/DragContext.tsx +75 -65
- package/apps/dashboard/components/DraggableCard.tsx +6 -46
- package/apps/dashboard/components/DropZone.tsx +2 -2
- package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
- package/apps/dashboard/components/EditableTitle.tsx +26 -6
- package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
- package/apps/dashboard/components/EpicGroup.tsx +329 -0
- package/apps/dashboard/components/GateCard.tsx +100 -16
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
- package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
- package/apps/dashboard/components/JettyLoader.tsx +38 -0
- package/apps/dashboard/components/KanbanBoard.tsx +147 -766
- package/apps/dashboard/components/KanbanCard.tsx +506 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
- package/apps/dashboard/components/MainNav.tsx +20 -54
- package/apps/dashboard/components/MessageBlock.tsx +391 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -15
- package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
- package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
- package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
- package/apps/dashboard/components/ReviewFooter.tsx +141 -0
- package/apps/dashboard/components/SessionList.tsx +19 -18
- package/apps/dashboard/components/SubscribeContent.tsx +206 -0
- package/apps/dashboard/components/TestTree.tsx +15 -14
- package/apps/dashboard/components/TipCard.tsx +177 -0
- package/apps/dashboard/components/Toast.tsx +5 -5
- package/apps/dashboard/components/TypeIcon.tsx +56 -0
- package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
- package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
- package/apps/dashboard/components/WorkItemTree.tsx +9 -28
- package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
- package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
- package/apps/dashboard/contexts/UsageContext.tsx +155 -0
- package/apps/dashboard/contexts/usageHelpers.js +9 -0
- package/apps/dashboard/electron/ipc-handlers.js +281 -88
- package/apps/dashboard/electron/main.js +691 -131
- package/apps/dashboard/electron/preload.js +25 -4
- package/apps/dashboard/electron/session-manager.js +163 -0
- package/apps/dashboard/electron-builder.config.js +3 -5
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/lib/backlog-parser.ts +50 -0
- package/apps/dashboard/lib/claude-process-manager.ts +50 -11
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/db-bridge.ts +33 -0
- package/apps/dashboard/lib/db.ts +136 -20
- package/apps/dashboard/lib/kanban-utils.ts +70 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/session-state-machine.ts +3 -0
- package/apps/dashboard/lib/session-stream-manager.ts +144 -38
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/tests.ts +3 -1
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next.config.js +35 -14
- package/apps/dashboard/package.json +6 -3
- package/apps/dashboard/public/bug-icon.svg +9 -0
- package/apps/dashboard/public/buoy-icon.svg +9 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.svg +9 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.svg +14 -0
- package/apps/dashboard/public/star-icon.svg +9 -0
- package/apps/dashboard/public/wrench-icon.svg +9 -0
- package/apps/dashboard/scripts/upload-to-r2.js +89 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
- package/apps/update-server/package.json +16 -0
- package/apps/update-server/schema.sql +31 -0
- package/apps/update-server/src/index.ts +1085 -0
- package/apps/update-server/tsconfig.json +16 -0
- package/apps/update-server/wrangler.toml +35 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +54 -116
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/migrations/027-plan-at-creation-column.js +33 -0
- package/lib/migrations/028-ready-for-review-column.js +27 -0
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +13 -6
- package/lib/seed-onboarding.js +101 -69
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +129 -16
- package/lib/work-tracking/index.js +86 -46
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +39 -28
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +131 -68
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/epic-planning/SKILL.md +68 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +83 -73
- package/skills-templates/production-mode/SKILL.md +49 -49
- package/skills-templates/request-routing/SKILL.md +27 -14
- package/skills-templates/simple-improvement/SKILL.md +68 -44
- package/skills-templates/speed-mode/SKILL.md +209 -128
- package/skills-templates/stable-mode/SKILL.md +105 -94
- package/templates/bdd-guidance.md +139 -0
- package/templates/bdd-scaffolding/wait.js +18 -0
- package/templates/bdd-scaffolding/world.js +19 -0
- package/.jettypod-backup/work.db +0 -0
- package/apps/dashboard/app/access-code/page.tsx +0 -110
- package/lib/discovery-checkpoint.js +0 -123
- package/skills-templates/project-discovery/SKILL.md +0 -372
|
@@ -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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
+
isTabSwitching: boolean;
|
|
64
|
+
}
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
91
|
+
const SessionStateContext = createContext<SessionStateContextValue | null>(null);
|
|
92
|
+
const SessionActionsContext = createContext<SessionActionsContextValue | null>(null);
|
|
93
|
+
const SessionPersistenceContext = createContext<SessionPersistenceContextValue | null>(null);
|
|
89
94
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 !==
|
|
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 !==
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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
|
-
//
|
|
451
|
-
|
|
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
|
-
//
|
|
476
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
764
|
-
|
|
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
|
-
//
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
834
|
-
|
|
908
|
+
setSessions(prev => {
|
|
909
|
+
const session = prev.get(currentActiveId);
|
|
910
|
+
if (!session) return prev;
|
|
835
911
|
|
|
836
|
-
|
|
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
|
|
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
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1315
|
+
isTabSwitching,
|
|
1316
|
+
}), [claudePanelOpen, sessions, activeSessionId, activeSession, standaloneSessions, activeStreamManager, isTabSwitching]);
|
|
1023
1317
|
|
|
1024
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
1048
|
-
{
|
|
1049
|
-
|
|
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
|
}
|