jettypod 4.4.120 → 4.4.121
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 +2 -1
- package/Cargo.lock +6450 -0
- package/Cargo.toml +35 -0
- package/README.md +5 -1
- package/TAURI-MIGRATION-PLAN.md +840 -0
- package/apps/dashboard/app/connect-claude/page.tsx +5 -6
- package/apps/dashboard/app/decision/[id]/page.tsx +54 -49
- package/apps/dashboard/app/demo/gates/page.tsx +3 -5
- package/apps/dashboard/app/design-system/page.tsx +1 -1
- package/apps/dashboard/app/globals.css +74 -2
- package/apps/dashboard/app/install-claude/page.tsx +3 -5
- package/apps/dashboard/app/login/page.tsx +17 -20
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +60 -12
- package/apps/dashboard/app/signup/page.tsx +14 -17
- package/apps/dashboard/app/subscribe/page.tsx +0 -2
- package/apps/dashboard/app/tests/page.tsx +37 -4
- package/apps/dashboard/app/welcome/page.tsx +12 -15
- package/apps/dashboard/app/work/[id]/page.tsx +90 -75
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +70 -61
- package/apps/dashboard/components/CardMenu.tsx +0 -1
- package/apps/dashboard/components/ClaudePanel.tsx +541 -283
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
- package/apps/dashboard/components/CopyableId.tsx +1 -2
- package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
- package/apps/dashboard/components/DragContext.tsx +132 -62
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +5 -6
- package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +0 -1
- package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
- package/apps/dashboard/components/EpicGroup.tsx +100 -70
- package/apps/dashboard/components/GateCard.tsx +0 -1
- package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
- package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
- package/apps/dashboard/components/JettyLoader.tsx +0 -1
- package/apps/dashboard/components/KanbanBoard.tsx +319 -173
- package/apps/dashboard/components/KanbanCard.tsx +341 -107
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
- package/apps/dashboard/components/MainNav.tsx +24 -25
- package/apps/dashboard/components/MessageBlock.tsx +93 -16
- package/apps/dashboard/components/ModeStartCard.tsx +0 -1
- package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
- package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
- package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
- package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
- package/apps/dashboard/components/ReviewFooter.tsx +12 -14
- package/apps/dashboard/components/SessionList.tsx +0 -1
- package/apps/dashboard/components/SubscribeContent.tsx +40 -11
- package/apps/dashboard/components/TestTree.tsx +1 -2
- package/apps/dashboard/components/TipCard.tsx +2 -4
- package/apps/dashboard/components/Toast.tsx +0 -1
- package/apps/dashboard/components/TypeIcon.tsx +7 -8
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
- package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
- package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
- package/apps/dashboard/components/WorkItemTree.tsx +2 -4
- package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
- package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
- package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +20 -73
- package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
- package/apps/dashboard/components/ui/Button.tsx +1 -1
- package/apps/dashboard/components/ui/Input.tsx +1 -1
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
- package/apps/dashboard/contexts/UsageContext.tsx +62 -31
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1302
- package/apps/dashboard/lib/environment-config.ts +173 -0
- package/apps/dashboard/lib/environment-verification.ts +119 -0
- package/apps/dashboard/lib/kanban-utils.ts +226 -26
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/service-recovery.ts +326 -0
- package/apps/dashboard/lib/session-state-machine.ts +1 -0
- package/apps/dashboard/lib/session-state-utils.ts +0 -164
- package/apps/dashboard/lib/session-stream-manager.ts +253 -122
- package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
- package/apps/dashboard/lib/tauri-bridge.ts +102 -0
- package/apps/dashboard/lib/tauri.ts +106 -0
- package/apps/dashboard/lib/utils.ts +3 -3
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -33
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/pier-icon.png +0 -0
- package/apps/dashboard/public/star-icon.png +0 -0
- package/apps/dashboard/public/wrench-icon.png +0 -0
- package/apps/dashboard/scripts/tauri-build.js +228 -0
- package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
- package/apps/dashboard/src/main.tsx +12 -0
- package/apps/dashboard/src/router.tsx +107 -0
- package/apps/dashboard/src/vite-env.d.ts +1 -0
- package/apps/dashboard/tsconfig.json +7 -12
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
- package/apps/dashboard/vite.config.ts +33 -0
- package/apps/update-server/src/index.ts +167 -30
- package/claude-hooks/global-guardrails.js +14 -13
- package/crates/jettypod-cli/Cargo.toml +19 -0
- package/crates/jettypod-cli/src/commands.rs +1249 -0
- package/crates/jettypod-cli/src/main.rs +595 -0
- package/crates/jettypod-core/Cargo.toml +26 -0
- package/crates/jettypod-core/build.rs +98 -0
- package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
- package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
- package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
- package/crates/jettypod-core/src/auth.rs +294 -0
- package/crates/jettypod-core/src/config.rs +397 -0
- package/crates/jettypod-core/src/db/mod.rs +507 -0
- package/crates/jettypod-core/src/db/recovery.rs +114 -0
- package/crates/jettypod-core/src/db/startup.rs +101 -0
- package/crates/jettypod-core/src/db/validate.rs +149 -0
- package/crates/jettypod-core/src/error.rs +76 -0
- package/crates/jettypod-core/src/git.rs +458 -0
- package/crates/jettypod-core/src/lib.rs +20 -0
- package/crates/jettypod-core/src/sessions.rs +625 -0
- package/crates/jettypod-core/src/skills.rs +556 -0
- package/crates/jettypod-core/src/work.rs +1086 -0
- package/crates/jettypod-core/src/worktree.rs +628 -0
- package/crates/jettypod-core/src/ws.rs +767 -0
- package/cucumber-test.cjs +6 -0
- package/jettypod.js +96 -4
- package/lib/bdd-preflight.js +96 -0
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/work-commands/index.js +58 -16
- package/lib/work-tracking/index.js +108 -8
- package/package.json +1 -1
- package/skills-templates/bug-mode/SKILL.md +43 -1
- package/skills-templates/chore-mode/SKILL.md +40 -1
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +14 -0
- package/skills-templates/feature-planning/SKILL.md +90 -1
- package/skills-templates/production-mode/SKILL.md +20 -0
- package/skills-templates/simple-improvement/SKILL.md +39 -2
- package/skills-templates/speed-mode/SKILL.md +10 -15
- package/skills-templates/stable-mode/SKILL.md +47 -0
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -525
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
- package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
- package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
- package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
- package/apps/dashboard/app/api/kanban/route.ts +0 -15
- package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
- package/apps/dashboard/app/api/settings/general/route.ts +0 -21
- package/apps/dashboard/app/api/tests/route.ts +0 -9
- package/apps/dashboard/app/api/tests/run/route.ts +0 -82
- package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
- package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
- package/apps/dashboard/app/api/usage/route.ts +0 -17
- package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/route.ts +0 -35
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -55
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
- package/apps/dashboard/electron/ipc-handlers.js +0 -1026
- package/apps/dashboard/electron/main.js +0 -2306
- package/apps/dashboard/electron/preload.js +0 -125
- package/apps/dashboard/electron/session-manager.js +0 -163
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/backlog-parser.ts +0 -50
- package/apps/dashboard/lib/claude-process-manager.ts +0 -529
- package/apps/dashboard/lib/db-bridge.ts +0 -283
- package/apps/dashboard/lib/prototypes.ts +0 -202
- package/apps/dashboard/lib/test-results-db.ts +0 -307
- package/apps/dashboard/lib/tests.ts +0 -282
- package/apps/dashboard/next.config.js +0 -66
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/bug-icon.svg +0 -9
- package/apps/dashboard/public/buoy-icon.svg +0 -9
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/in-flight-seagull.svg +0 -9
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/pier-icon.svg +0 -14
- package/apps/dashboard/public/star-icon.svg +0 -9
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/public/wrench-icon.svg +0 -9
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
2
|
import { createContext, useContext, useState, useCallback, useEffect, useRef, useMemo, type ReactNode } from 'react';
|
|
3
|
+
import { invoke, invokeWithTimeout } from '../lib/tauri';
|
|
4
4
|
import {
|
|
5
5
|
type ClaudeMessage,
|
|
6
6
|
type StreamStatus,
|
|
@@ -40,6 +40,7 @@ export interface Session {
|
|
|
40
40
|
error: string | null;
|
|
41
41
|
exitCode: number | null;
|
|
42
42
|
narratedMode: boolean;
|
|
43
|
+
fullReadoutMode: boolean;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
// Split into 3 contexts to prevent unnecessary re-renders:
|
|
@@ -60,6 +61,8 @@ interface SessionStateContextValue {
|
|
|
60
61
|
canRetry: boolean;
|
|
61
62
|
queuedMessage: QueuedMessage | null;
|
|
62
63
|
narratedMode: boolean;
|
|
64
|
+
fullReadoutMode: boolean;
|
|
65
|
+
rawEvents: unknown[];
|
|
63
66
|
isTabSwitching: boolean;
|
|
64
67
|
}
|
|
65
68
|
|
|
@@ -67,17 +70,21 @@ interface SessionActionsContextValue {
|
|
|
67
70
|
setClaudePanelOpen: (open: boolean) => void;
|
|
68
71
|
openSession: (id: string, title: string, type?: string, conversational?: boolean, description?: string | null, initialHidden?: boolean) => void;
|
|
69
72
|
switchSession: (id: string) => void;
|
|
70
|
-
closeSession: (sessionId: string) => void
|
|
73
|
+
closeSession: (sessionId: string) => Promise<void>;
|
|
71
74
|
openSessionPanel: () => void;
|
|
72
75
|
createNewSession: () => Promise<void>;
|
|
73
76
|
createAddToBacklogSession: () => Promise<void>;
|
|
74
77
|
createRunScenarioSession: (featureFile: string, scenarioTitle: string) => Promise<void>;
|
|
75
78
|
createFixScenarioSession: (featureFile: string, scenarioTitle: string, error: string, failedStep?: string, steps?: string[]) => Promise<void>;
|
|
79
|
+
createFixServiceSession: (crashedServices: { name: string; port: number | null }[]) => Promise<void>;
|
|
80
|
+
createBddSetupSession: (setupPrompt: string) => Promise<void>;
|
|
76
81
|
createWelcomeSession: () => Promise<void>;
|
|
82
|
+
createSkillSession: (skillName: string, title: string, customMessage?: string) => Promise<string>;
|
|
77
83
|
sendMessage: (message: string, images?: AttachedImage[]) => void;
|
|
78
84
|
retry: () => void;
|
|
79
85
|
stop: () => void;
|
|
80
86
|
toggleNarratedMode: () => void;
|
|
87
|
+
toggleFullReadout: () => void;
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
interface SessionPersistenceContextValue {
|
|
@@ -164,29 +171,8 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
164
171
|
sessionsRef.current = sessions;
|
|
165
172
|
|
|
166
173
|
// Wrapper to update both React state and sync ref
|
|
167
|
-
//
|
|
174
|
+
// Pin/unpin removed — no longer needed in Tauri (no idle process cleanup)
|
|
168
175
|
const setActiveSessionId = useCallback((id: string | null) => {
|
|
169
|
-
const previousId = sessionRefs.activeSessionId.current;
|
|
170
|
-
const currentSessions = sessionsRef.current;
|
|
171
|
-
|
|
172
|
-
// Unpin previous session (allow idle cleanup)
|
|
173
|
-
if (previousId && previousId !== id) {
|
|
174
|
-
const session = currentSessions.get(previousId);
|
|
175
|
-
const endpoint = session?.type === 'workitem'
|
|
176
|
-
? `/api/claude/${previousId}/pin`
|
|
177
|
-
: `/api/claude/sessions/${previousId}/pin`;
|
|
178
|
-
fetch(endpoint, { method: 'DELETE' }).catch(() => {});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Pin new session (prevent idle cleanup)
|
|
182
|
-
if (id) {
|
|
183
|
-
const session = currentSessions.get(id);
|
|
184
|
-
const endpoint = session?.type === 'workitem'
|
|
185
|
-
? `/api/claude/${id}/pin`
|
|
186
|
-
: `/api/claude/sessions/${id}/pin`;
|
|
187
|
-
fetch(endpoint, { method: 'POST' }).catch(() => {});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
176
|
sessionRefs.activeSessionId.set(id);
|
|
191
177
|
setActiveSessionIdState(id);
|
|
192
178
|
}, [sessionRefs]);
|
|
@@ -194,18 +180,47 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
194
180
|
// Tab-switching UX refs (NOT stream ownership - each session owns its own stream)
|
|
195
181
|
// Trailing-edge debounce for rapid session switches (#1000100)
|
|
196
182
|
const switchDebounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
197
|
-
// AbortController for cancelling in-flight historical content fetches (#1000101)
|
|
198
|
-
const contentFetchAbortRef = useRef<AbortController | null>(null);
|
|
199
|
-
|
|
200
183
|
// Reactive tab-switching state (exposed to UI so components can suppress flash of empty state)
|
|
201
184
|
const [isTabSwitching, setIsTabSwitching] = useState(false);
|
|
202
185
|
|
|
203
|
-
// Force re-render when stream state changes (since stream managers are mutable)
|
|
204
|
-
const [, forceUpdate] = useState({});
|
|
205
|
-
|
|
206
186
|
// Standalone sessions state
|
|
207
187
|
const [standaloneSessions, setStandaloneSessions] = useState<SessionItem[]>([]);
|
|
208
188
|
|
|
189
|
+
// Cache: work item ID → DB session ID (avoids N+1 db_get_sessions_for_work_item calls)
|
|
190
|
+
const dbSessionIdCache = useRef(new Map<string, number>());
|
|
191
|
+
|
|
192
|
+
// Persist session messages to DB with proper error handling
|
|
193
|
+
const persistSessionMessages = useCallback(async (sessionId: string, messages: ClaudeMessage[]) => {
|
|
194
|
+
if (messages.length === 0) return;
|
|
195
|
+
const session = sessionsRef.current.get(sessionId);
|
|
196
|
+
try {
|
|
197
|
+
if (session?.type === 'standalone') {
|
|
198
|
+
await invoke('db_set_session_content', {
|
|
199
|
+
id: Number(sessionId),
|
|
200
|
+
content: JSON.stringify(messages),
|
|
201
|
+
});
|
|
202
|
+
} else {
|
|
203
|
+
// Work-item session: check cache first, then look up
|
|
204
|
+
let dbId = dbSessionIdCache.current.get(sessionId);
|
|
205
|
+
if (!dbId) {
|
|
206
|
+
const linked = await invoke<any[]>('db_get_sessions_for_work_item', { workItemId: Number(sessionId) });
|
|
207
|
+
if (linked?.[0]?.id) {
|
|
208
|
+
dbId = linked[0].id;
|
|
209
|
+
dbSessionIdCache.current.set(sessionId, dbId);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (dbId) {
|
|
213
|
+
await invoke('db_set_session_content', {
|
|
214
|
+
id: dbId,
|
|
215
|
+
content: JSON.stringify(messages),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error('Failed to persist session messages:', sessionId, err);
|
|
221
|
+
}
|
|
222
|
+
}, []);
|
|
223
|
+
|
|
209
224
|
// Subscribe to registry events to sync React state with registry
|
|
210
225
|
useEffect(() => {
|
|
211
226
|
const handleStateChange = (sessionId: string, state: StreamState) => {
|
|
@@ -221,6 +236,8 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
221
236
|
machine.send('CONNECTED');
|
|
222
237
|
} else if (state.status === 'done') {
|
|
223
238
|
machine.send('COMPLETE');
|
|
239
|
+
// Persist messages to DB so they survive page refresh
|
|
240
|
+
persistSessionMessages(sessionId, state.messages);
|
|
224
241
|
// Auto-send queued message after stream completes
|
|
225
242
|
if (state.queuedMessage) {
|
|
226
243
|
const streamManager = registry.get(sessionId);
|
|
@@ -234,6 +251,8 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
234
251
|
}
|
|
235
252
|
} else if (state.status === 'error') {
|
|
236
253
|
machine.send('ERROR');
|
|
254
|
+
// Persist messages on error too so user can see what happened after refresh
|
|
255
|
+
persistSessionMessages(sessionId, state.messages);
|
|
237
256
|
} else if (state.status === 'idle' && machine.state !== 'idle') {
|
|
238
257
|
// Force to idle if we got out of sync
|
|
239
258
|
machine.forceState('idle');
|
|
@@ -248,23 +267,34 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
248
267
|
}
|
|
249
268
|
}
|
|
250
269
|
|
|
251
|
-
// Update React state
|
|
252
|
-
setSessions
|
|
253
|
-
|
|
254
|
-
|
|
270
|
+
// Update React state (skip if nothing changed to avoid unnecessary re-renders)
|
|
271
|
+
// Check BEFORE calling setSessions to avoid queueing a no-op state update
|
|
272
|
+
const currentSession = sessionsRef.current.get(sessionId);
|
|
273
|
+
if (!currentSession) return;
|
|
274
|
+
if (
|
|
275
|
+
currentSession.messages === state.messages &&
|
|
276
|
+
currentSession.status === state.status &&
|
|
277
|
+
currentSession.error === state.error &&
|
|
278
|
+
currentSession.exitCode === state.exitCode &&
|
|
279
|
+
currentSession.narratedMode === state.narratedMode &&
|
|
280
|
+
currentSession.fullReadoutMode === state.fullReadoutMode
|
|
281
|
+
) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
255
284
|
|
|
285
|
+
setSessions(prev => {
|
|
256
286
|
const updated = new Map(prev);
|
|
257
287
|
updated.set(sessionId, {
|
|
258
|
-
...
|
|
288
|
+
...currentSession,
|
|
259
289
|
messages: state.messages,
|
|
260
290
|
status: state.status,
|
|
261
291
|
error: state.error,
|
|
262
292
|
exitCode: state.exitCode,
|
|
263
293
|
narratedMode: state.narratedMode,
|
|
294
|
+
fullReadoutMode: state.fullReadoutMode,
|
|
264
295
|
});
|
|
265
296
|
return updated;
|
|
266
297
|
});
|
|
267
|
-
forceUpdate({});
|
|
268
298
|
};
|
|
269
299
|
|
|
270
300
|
registry.on('stateChange', handleStateChange);
|
|
@@ -272,8 +302,9 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
272
302
|
|
|
273
303
|
return () => {
|
|
274
304
|
registry.off('stateChange', handleStateChange);
|
|
305
|
+
registry.stopCleanup();
|
|
275
306
|
};
|
|
276
|
-
}, [registry, sessionRefs, getStateMachine, messageBuffer]);
|
|
307
|
+
}, [registry, sessionRefs, getStateMachine, messageBuffer, persistSessionMessages]);
|
|
277
308
|
|
|
278
309
|
// Persist active session ID to sessionStorage
|
|
279
310
|
useEffect(() => {
|
|
@@ -292,7 +323,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
292
323
|
const getOrCreateStreamManager = useCallback((
|
|
293
324
|
sessionId: string,
|
|
294
325
|
standalone: boolean,
|
|
295
|
-
onWorkItemCreated?: (workItemId: number, title: string) => void,
|
|
326
|
+
onWorkItemCreated?: (workItemId: number, title: string, sourceSessionId: string) => void,
|
|
296
327
|
conversational?: boolean
|
|
297
328
|
) => {
|
|
298
329
|
// Registry handles idempotent creation and state change events
|
|
@@ -315,154 +346,206 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
315
346
|
if (!sourceSessionId) return;
|
|
316
347
|
|
|
317
348
|
try {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
sessionId: parseInt(sourceSessionId, 10),
|
|
323
|
-
workItemId,
|
|
324
|
-
}),
|
|
349
|
+
await invoke('db_link_claude_session', {
|
|
350
|
+
sessionId: parseInt(sourceSessionId, 10),
|
|
351
|
+
workItemId: Number(workItemId),
|
|
352
|
+
title,
|
|
325
353
|
});
|
|
354
|
+
} catch (err) {
|
|
355
|
+
// DB link failed — don't re-key registry or state will be inconsistent
|
|
356
|
+
console.error('Failed to link session in DB:', sourceSessionId, '->', workItemId, err);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
326
359
|
|
|
327
|
-
|
|
328
|
-
|
|
360
|
+
try {
|
|
361
|
+
const newSessionId = String(workItemId);
|
|
329
362
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
363
|
+
// Update stream manager's session context via registry
|
|
364
|
+
const streamManager = registry.get(sourceSessionId);
|
|
365
|
+
if (streamManager) {
|
|
366
|
+
streamManager.updateSessionContext({
|
|
367
|
+
workItemId: newSessionId,
|
|
368
|
+
standalone: false,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
338
371
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (!session) return prev;
|
|
372
|
+
// Re-key the registry entry so the onStateChange closure emits
|
|
373
|
+
// with the new session ID (fixes stream freeze after linking #1115)
|
|
374
|
+
registry.rekey(sourceSessionId, newSessionId);
|
|
343
375
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
376
|
+
// Atomic session update (#1000102): Build new map in one pass
|
|
377
|
+
setSessions(prev => {
|
|
378
|
+
const session = prev.get(sourceSessionId);
|
|
379
|
+
if (!session) return prev;
|
|
380
|
+
|
|
381
|
+
// Build new map: copy all except old session, add new session
|
|
382
|
+
const updated = new Map<string, Session>();
|
|
383
|
+
for (const [key, value] of prev) {
|
|
384
|
+
if (key !== sourceSessionId) {
|
|
385
|
+
updated.set(key, value);
|
|
350
386
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return updated;
|
|
387
|
+
}
|
|
388
|
+
updated.set(newSessionId, {
|
|
389
|
+
...session,
|
|
390
|
+
id: newSessionId,
|
|
391
|
+
type: 'workitem',
|
|
392
|
+
title,
|
|
358
393
|
});
|
|
394
|
+
return updated;
|
|
395
|
+
});
|
|
359
396
|
|
|
360
|
-
|
|
361
|
-
|
|
397
|
+
// Update active session ID to the work item ID
|
|
398
|
+
setActiveSessionId(newSessionId);
|
|
362
399
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
400
|
+
// Remove from standalone sessions list
|
|
401
|
+
setStandaloneSessions(prev =>
|
|
402
|
+
prev.filter(s => s.id !== sourceSessionId)
|
|
403
|
+
);
|
|
368
404
|
} catch (err) {
|
|
369
|
-
console.error('Failed to
|
|
405
|
+
console.error('Failed to re-key session after link:', err);
|
|
370
406
|
}
|
|
371
407
|
}, [registry, setActiveSessionId, refreshUsage]);
|
|
372
408
|
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
409
|
+
// Track whether sessions have been loaded successfully
|
|
410
|
+
const sessionsLoadedRef = useRef(false);
|
|
411
|
+
// Promise deduplication: if loadSessions is already in flight, return the existing promise
|
|
412
|
+
const loadSessionsPromiseRef = useRef<Promise<void> | null>(null);
|
|
413
|
+
|
|
414
|
+
// Load persisted sessions from backend.
|
|
415
|
+
// Retries silently when DB isn't initialized yet (e.g., no project open).
|
|
416
|
+
// Deduplicates concurrent calls (e.g., Strict Mode double-mount).
|
|
417
|
+
const loadSessions = useCallback(async () => {
|
|
418
|
+
if (loadSessionsPromiseRef.current) {
|
|
419
|
+
return loadSessionsPromiseRef.current;
|
|
420
|
+
}
|
|
421
|
+
const doLoad = async () => {
|
|
376
422
|
try {
|
|
377
|
-
|
|
378
|
-
|
|
423
|
+
const persistedSessions = await invokeWithTimeout<any[]>('db_get_all_sessions', undefined, 10000);
|
|
424
|
+
if (!Array.isArray(persistedSessions)) return;
|
|
425
|
+
|
|
426
|
+
sessionsLoadedRef.current = true;
|
|
427
|
+
|
|
428
|
+
const workItemSessions: typeof persistedSessions = [];
|
|
429
|
+
const standaloneItems: SessionItem[] = [];
|
|
430
|
+
// Map session DB id → parsed messages for content restoration
|
|
431
|
+
const contentMap = new Map<string, ClaudeMessage[]>();
|
|
432
|
+
|
|
433
|
+
for (const session of persistedSessions) {
|
|
434
|
+
if (!session) continue;
|
|
435
|
+
|
|
436
|
+
// Parse persisted content if available
|
|
437
|
+
if (session.content) {
|
|
438
|
+
try {
|
|
439
|
+
const parsed = JSON.parse(session.content);
|
|
440
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
441
|
+
const key = session.work_item_id ? String(session.work_item_id) : String(session.id);
|
|
442
|
+
contentMap.set(key, parsed);
|
|
443
|
+
}
|
|
444
|
+
} catch { /* ignore malformed content */ }
|
|
445
|
+
}
|
|
379
446
|
|
|
380
|
-
|
|
381
|
-
|
|
447
|
+
if (session.work_item_id) {
|
|
448
|
+
if (session.title) {
|
|
449
|
+
workItemSessions.push(session);
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
standaloneItems.push({
|
|
453
|
+
id: String(session.id),
|
|
454
|
+
title: session.session_title || session.title || 'Untitled Session',
|
|
455
|
+
featureId: null,
|
|
456
|
+
featureTitle: null,
|
|
457
|
+
updatedAt: session.completed_at || session.started_at,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
382
461
|
|
|
383
|
-
|
|
384
|
-
|
|
462
|
+
// Add all sessions (work item + standalone) to sessions Map for state management
|
|
463
|
+
setSessions(prev => {
|
|
464
|
+
const updated = new Map(prev);
|
|
385
465
|
|
|
386
|
-
|
|
387
|
-
|
|
466
|
+
// Add work item sessions (stream manager created lazily on first use)
|
|
467
|
+
for (const session of workItemSessions) {
|
|
468
|
+
const sessionId = String(session.work_item_id);
|
|
469
|
+
if (updated.has(sessionId)) continue;
|
|
388
470
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
standaloneItems.push({
|
|
395
|
-
id: session.id, // API returns string IDs
|
|
396
|
-
title: session.session_title || session.title || 'Untitled Session',
|
|
397
|
-
featureId: null,
|
|
398
|
-
featureTitle: null,
|
|
399
|
-
updatedAt: session.updatedAt,
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
}
|
|
471
|
+
const status = session.status;
|
|
472
|
+
const frontendStatus = status === 'completed' ? 'done'
|
|
473
|
+
: status === 'error' ? 'error'
|
|
474
|
+
: 'idle';
|
|
475
|
+
const restoredMessages = contentMap.get(sessionId) ?? [];
|
|
403
476
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
: 'idle';
|
|
417
|
-
|
|
418
|
-
updated.set(sessionId, {
|
|
419
|
-
id: sessionId,
|
|
420
|
-
title: session.title,
|
|
421
|
-
type: 'workitem',
|
|
422
|
-
messages: [],
|
|
423
|
-
status: frontendStatus as StreamStatus,
|
|
424
|
-
error: null,
|
|
425
|
-
exitCode: status === 'completed' ? 0 : status === 'error' ? 1 : null,
|
|
426
|
-
narratedMode: true,
|
|
427
|
-
// Stream manager created lazily when session becomes active
|
|
428
|
-
});
|
|
429
|
-
}
|
|
477
|
+
updated.set(sessionId, {
|
|
478
|
+
id: sessionId,
|
|
479
|
+
title: session.title,
|
|
480
|
+
type: 'workitem',
|
|
481
|
+
messages: restoredMessages,
|
|
482
|
+
status: restoredMessages.length > 0 ? 'done' as StreamStatus : frontendStatus as StreamStatus,
|
|
483
|
+
error: null,
|
|
484
|
+
exitCode: status === 'completed' ? 0 : status === 'error' ? 1 : null,
|
|
485
|
+
narratedMode: true,
|
|
486
|
+
fullReadoutMode: false,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
430
489
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
490
|
+
// Add standalone sessions (stream manager created lazily on first use)
|
|
491
|
+
for (const session of standaloneItems) {
|
|
492
|
+
if (updated.has(session.id)) continue;
|
|
493
|
+
const restoredMessages = contentMap.get(session.id) ?? [];
|
|
494
|
+
|
|
495
|
+
updated.set(session.id, {
|
|
496
|
+
id: session.id,
|
|
497
|
+
title: session.title,
|
|
498
|
+
type: 'standalone',
|
|
499
|
+
messages: restoredMessages,
|
|
500
|
+
status: restoredMessages.length > 0 ? 'done' as StreamStatus : 'idle',
|
|
501
|
+
error: null,
|
|
502
|
+
exitCode: null,
|
|
503
|
+
narratedMode: session.title !== 'Welcome',
|
|
504
|
+
fullReadoutMode: false,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
448
507
|
|
|
449
|
-
|
|
450
|
-
|
|
508
|
+
return updated;
|
|
509
|
+
});
|
|
451
510
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
511
|
+
setStandaloneSessions(standaloneItems);
|
|
512
|
+
|
|
513
|
+
// Auto-select active session if none is set (prevents "no active session" on sendMessage)
|
|
514
|
+
if (!sessionRefs.activeSessionId.current) {
|
|
515
|
+
const allSessionIds = [
|
|
516
|
+
...workItemSessions.map(s => String(s.work_item_id)),
|
|
517
|
+
...standaloneItems.map(s => s.id),
|
|
518
|
+
];
|
|
519
|
+
const savedId = sessionStorage.getItem(ACTIVE_SESSION_KEY);
|
|
520
|
+
if (savedId && allSessionIds.includes(savedId)) {
|
|
521
|
+
setActiveSessionId(savedId);
|
|
522
|
+
} else if (allSessionIds.length > 0) {
|
|
523
|
+
setActiveSessionId(allSessionIds[0]);
|
|
524
|
+
}
|
|
455
525
|
}
|
|
526
|
+
} catch {
|
|
527
|
+
// Silently ignore — DB may not be initialized yet (no project open).
|
|
528
|
+
// Sessions will be loaded when openSessionPanel triggers loadSessions.
|
|
456
529
|
}
|
|
530
|
+
};
|
|
531
|
+
loadSessionsPromiseRef.current = doLoad();
|
|
532
|
+
try {
|
|
533
|
+
await loadSessionsPromiseRef.current;
|
|
534
|
+
} finally {
|
|
535
|
+
loadSessionsPromiseRef.current = null;
|
|
536
|
+
}
|
|
537
|
+
}, [sessionRefs, setActiveSessionId]);
|
|
538
|
+
|
|
539
|
+
// Load sessions on mount (may fail if no project is open yet — that's OK)
|
|
540
|
+
useEffect(() => {
|
|
457
541
|
loadSessions();
|
|
458
|
-
}, []);
|
|
542
|
+
}, [loadSessions]);
|
|
459
543
|
|
|
460
544
|
// Helper: Ensure session has a stream manager in registry (lazy creation)
|
|
461
545
|
const ensureStreamManager = useCallback((sessionId: string, session: Session) => {
|
|
462
546
|
// Check registry first (idempotent - returns existing if present)
|
|
463
547
|
const existing = registry.get(sessionId);
|
|
464
548
|
if (existing) {
|
|
465
|
-
registry.acquire(sessionId); // Track reference for lifecycle
|
|
466
549
|
return existing;
|
|
467
550
|
}
|
|
468
551
|
|
|
@@ -472,7 +555,6 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
472
555
|
session.type === 'standalone',
|
|
473
556
|
session.type === 'standalone' ? handleWorkItemCreated : undefined
|
|
474
557
|
);
|
|
475
|
-
registry.acquire(sessionId); // Track reference for lifecycle
|
|
476
558
|
|
|
477
559
|
return streamManager;
|
|
478
560
|
}, [registry, getOrCreateStreamManager, handleWorkItemCreated]);
|
|
@@ -498,7 +580,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
498
580
|
|
|
499
581
|
// Create stream manager in registry — pass conversational flag for skip-delay behavior
|
|
500
582
|
const streamManager = getOrCreateStreamManager(id, false, undefined, conversational);
|
|
501
|
-
registry.
|
|
583
|
+
registry.recordActivity(id);
|
|
502
584
|
|
|
503
585
|
const newSession: Session = {
|
|
504
586
|
id,
|
|
@@ -509,6 +591,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
509
591
|
error: null,
|
|
510
592
|
exitCode: null,
|
|
511
593
|
narratedMode: true,
|
|
594
|
+
fullReadoutMode: false,
|
|
512
595
|
};
|
|
513
596
|
|
|
514
597
|
setSessions(prev => {
|
|
@@ -553,128 +636,115 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
553
636
|
messageBuffer.startBuffering(previousSessionId);
|
|
554
637
|
}
|
|
555
638
|
|
|
556
|
-
//
|
|
639
|
+
// Cancel any in-flight switch so rapid clicks don't cause stale loads
|
|
557
640
|
if (switchDebounceTimeoutRef.current) {
|
|
558
641
|
clearTimeout(switchDebounceTimeoutRef.current);
|
|
642
|
+
switchDebounceTimeoutRef.current = null;
|
|
559
643
|
}
|
|
560
644
|
|
|
561
645
|
// Immediately update visual state (active tab) for responsiveness
|
|
562
646
|
switchingToRef.current = id;
|
|
563
647
|
setActiveSessionId(id);
|
|
564
648
|
|
|
565
|
-
//
|
|
566
|
-
|
|
567
|
-
|
|
649
|
+
// Load content immediately (no debounce — local SQLite IPC is fast)
|
|
650
|
+
const session = sessions.get(id)!;
|
|
651
|
+
const isStandalone = session.type === 'standalone';
|
|
568
652
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (!sessions.has(id)) return;
|
|
653
|
+
// Ensure session has a stream manager in registry
|
|
654
|
+
const streamManager = ensureStreamManager(id, session);
|
|
572
655
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
// Use sync ref to check streaming status (avoids stale closure)
|
|
580
|
-
const isActivelyStreaming = isSessionStreaming(sessionRefs, id) ||
|
|
581
|
-
session.status === 'streaming' ||
|
|
582
|
-
streamManager.status === 'streaming';
|
|
583
|
-
const hasMessages = session.messages.length > 0 || streamManager.messages.length > 0;
|
|
584
|
-
|
|
585
|
-
// Only load from DB if session is idle with no messages
|
|
586
|
-
if (!isActivelyStreaming && !hasMessages) {
|
|
587
|
-
const queryParam = isStandalone ? '' : '?by=workitem';
|
|
588
|
-
try {
|
|
589
|
-
// Cancel any in-flight content fetch (#1000101)
|
|
590
|
-
if (contentFetchAbortRef.current) {
|
|
591
|
-
contentFetchAbortRef.current.abort();
|
|
592
|
-
}
|
|
593
|
-
contentFetchAbortRef.current = new AbortController();
|
|
656
|
+
// Use sync ref to check streaming status (avoids stale closure)
|
|
657
|
+
const isActivelyStreaming = isSessionStreaming(sessionRefs, id) ||
|
|
658
|
+
session.status === 'streaming' ||
|
|
659
|
+
streamManager.status === 'streaming';
|
|
660
|
+
const hasMessages = session.messages.length > 0 || streamManager.messages.length > 0;
|
|
594
661
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
662
|
+
// Only load from DB if session is idle with no messages
|
|
663
|
+
if (!isActivelyStreaming && !hasMessages) {
|
|
664
|
+
try {
|
|
665
|
+
// Look up the session's DB ID: for standalone sessions use the id directly,
|
|
666
|
+
// for work-item sessions look up sessions linked to that work item
|
|
667
|
+
let content: any[] | null = null;
|
|
668
|
+
|
|
669
|
+
if (isStandalone) {
|
|
670
|
+
const sessionData = await invoke<any>('db_get_session', { id: Number(id) });
|
|
671
|
+
if (sessionData?.content) {
|
|
672
|
+
try { content = JSON.parse(sessionData.content); } catch { content = null; }
|
|
673
|
+
}
|
|
674
|
+
} else {
|
|
675
|
+
const linkedSessions = await invoke<any[]>('db_get_sessions_for_work_item', { workItemId: Number(id) });
|
|
676
|
+
if (linkedSessions && linkedSessions.length > 0 && linkedSessions[0].content) {
|
|
677
|
+
try { content = JSON.parse(linkedSessions[0].content); } catch { content = null; }
|
|
678
|
+
}
|
|
679
|
+
}
|
|
601
680
|
|
|
602
|
-
|
|
603
|
-
|
|
681
|
+
// Check if user switched to a different session while we were fetching
|
|
682
|
+
if (switchingToRef.current !== id) return;
|
|
604
683
|
|
|
605
|
-
|
|
606
|
-
|
|
684
|
+
// Re-check streaming status using sync ref (most current)
|
|
685
|
+
const currentlyStreaming = isSessionStreaming(sessionRefs, id);
|
|
686
|
+
const currentSession = sessions.get(id);
|
|
687
|
+
const currentManager = registry.get(id);
|
|
688
|
+
const nowHasMessages = (currentSession?.messages.length ?? 0) > 0 ||
|
|
689
|
+
(currentManager?.messages.length ?? 0) > 0;
|
|
607
690
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const nowHasMessages = (currentSession?.messages.length ?? 0) > 0 ||
|
|
613
|
-
(currentManager?.messages.length ?? 0) > 0;
|
|
691
|
+
if (currentlyStreaming || nowHasMessages) {
|
|
692
|
+
// Session became active during fetch - don't overwrite
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
614
695
|
|
|
615
|
-
|
|
616
|
-
|
|
696
|
+
if (content && Array.isArray(content) && content.length > 0) {
|
|
697
|
+
// Update stream manager via registry
|
|
698
|
+
const mgr = registry.get(id);
|
|
699
|
+
if (mgr) {
|
|
700
|
+
// Final safety check via sync ref
|
|
701
|
+
if (isSessionStreaming(sessionRefs, id)) {
|
|
617
702
|
return;
|
|
618
703
|
}
|
|
704
|
+
mgr.setMessages(content);
|
|
705
|
+
mgr.setStatus('done');
|
|
706
|
+
}
|
|
619
707
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
mgr.setMessages(content);
|
|
629
|
-
mgr.setStatus('done');
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Update session React state
|
|
633
|
-
setSessions(prev => {
|
|
634
|
-
const updated = new Map(prev);
|
|
635
|
-
const existing = updated.get(id);
|
|
636
|
-
if (existing) {
|
|
637
|
-
updated.set(id, { ...existing, messages: content, status: 'done' });
|
|
638
|
-
}
|
|
639
|
-
return updated;
|
|
640
|
-
});
|
|
708
|
+
// Update session React state
|
|
709
|
+
setSessions(prev => {
|
|
710
|
+
const updated = new Map(prev);
|
|
711
|
+
const existing = updated.get(id);
|
|
712
|
+
if (existing) {
|
|
713
|
+
updated.set(id, { ...existing, messages: content!, status: 'done' });
|
|
641
714
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
// Ignore abort errors - they're expected when user switches tabs (#1000101)
|
|
645
|
-
if (err instanceof Error && err.name === 'AbortError') {
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
console.error('[ClaudeSessionContext] Failed to load session content:', err);
|
|
715
|
+
return updated;
|
|
716
|
+
});
|
|
649
717
|
}
|
|
718
|
+
} catch (err) {
|
|
719
|
+
console.error('[ClaudeSessionContext] Failed to load session content:', err);
|
|
650
720
|
}
|
|
721
|
+
}
|
|
651
722
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
}
|
|
723
|
+
// Flush buffered messages for the previous session
|
|
724
|
+
if (previousSessionId && previousSessionId !== id) {
|
|
725
|
+
const bufferedMessages = messageBuffer.flushBuffer(previousSessionId);
|
|
726
|
+
if (bufferedMessages.length > 0) {
|
|
727
|
+
const prevManager = registry.get(previousSessionId);
|
|
728
|
+
if (prevManager) {
|
|
729
|
+
// Apply buffered messages to the stream manager
|
|
730
|
+
const existingMessages = prevManager.messages;
|
|
731
|
+
prevManager.setMessages([...existingMessages, ...bufferedMessages]);
|
|
662
732
|
}
|
|
663
|
-
messageBuffer.stopBuffering(previousSessionId);
|
|
664
733
|
}
|
|
734
|
+
messageBuffer.stopBuffering(previousSessionId);
|
|
735
|
+
}
|
|
665
736
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}, 100); // 100ms debounce delay
|
|
737
|
+
// Clear tracking ref when switch completes
|
|
738
|
+
if (switchingToRef.current === id) {
|
|
739
|
+
switchingToRef.current = null;
|
|
740
|
+
}
|
|
741
|
+
sessionRefs.isTabSwitching.set(false);
|
|
742
|
+
setIsTabSwitching(false);
|
|
673
743
|
}, [sessions, sessionRefs, registry, ensureStreamManager, setActiveSessionId, messageBuffer]);
|
|
674
744
|
|
|
675
745
|
// Close a session
|
|
676
746
|
// With per-session streams, we delete the stream manager from registry when closing
|
|
677
|
-
const closeSession = useCallback((sessionId: string) => {
|
|
747
|
+
const closeSession = useCallback(async (sessionId: string) => {
|
|
678
748
|
// Get session to determine API type
|
|
679
749
|
const session = sessions.get(sessionId);
|
|
680
750
|
const sessionType = session?.type || 'standalone';
|
|
@@ -685,7 +755,14 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
685
755
|
// Clear streaming status in sync ref
|
|
686
756
|
setSessionStreaming(sessionRefs, sessionId, false);
|
|
687
757
|
|
|
688
|
-
//
|
|
758
|
+
// Persist close to DB first — prevents sessions reappearing after refresh
|
|
759
|
+
try {
|
|
760
|
+
await invokeWithTimeout('db_close_claude_session', { id: Number(sessionId) }, 5000);
|
|
761
|
+
} catch (err) {
|
|
762
|
+
console.error('Failed to close session in DB:', sessionId, err);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Update UI state after DB persistence
|
|
689
766
|
setStandaloneSessions(prev => prev.filter(s => s.id !== sessionId));
|
|
690
767
|
setSessions(prev => {
|
|
691
768
|
const updated = new Map(prev);
|
|
@@ -709,17 +786,6 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
709
786
|
setActiveSessionId(remaining[Math.min(newIndex, remaining.length - 1)]);
|
|
710
787
|
}
|
|
711
788
|
}
|
|
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
|
-
});
|
|
723
789
|
}, [activeSessionId, sessions, standaloneSessions, registry, sessionRefs, setActiveSessionId]);
|
|
724
790
|
|
|
725
791
|
// Create a new standalone session
|
|
@@ -730,25 +796,12 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
730
796
|
}
|
|
731
797
|
|
|
732
798
|
try {
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
body: JSON.stringify({ title: 'New Session' }),
|
|
799
|
+
const sessionId = await invoke<number>('db_create_claude_session', {
|
|
800
|
+
title: 'New Session',
|
|
801
|
+
sessionTitle: null,
|
|
737
802
|
});
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
const errorData = await response.json();
|
|
741
|
-
if (errorData.code === 'SESSION_LIMIT_REACHED') {
|
|
742
|
-
console.warn(`[ClaudeSessionContext] ${errorData.error}`);
|
|
743
|
-
showToast('Session limit reached. Close existing sessions to create new ones.', 'error');
|
|
744
|
-
} else {
|
|
745
|
-
console.error('[ClaudeSessionContext] Failed to create session:', errorData.error);
|
|
746
|
-
}
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
const { id, title } = await response.json();
|
|
751
|
-
// API returns string IDs
|
|
803
|
+
const id = String(sessionId);
|
|
804
|
+
const title = 'New Session';
|
|
752
805
|
|
|
753
806
|
setStandaloneSessions(prev => [...prev, {
|
|
754
807
|
id,
|
|
@@ -759,7 +812,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
759
812
|
|
|
760
813
|
// Create stream manager in registry
|
|
761
814
|
getOrCreateStreamManager(id, true, handleWorkItemCreated);
|
|
762
|
-
registry.
|
|
815
|
+
registry.recordActivity(id);
|
|
763
816
|
|
|
764
817
|
const newSession: Session = {
|
|
765
818
|
id,
|
|
@@ -770,6 +823,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
770
823
|
error: null,
|
|
771
824
|
exitCode: null,
|
|
772
825
|
narratedMode: true,
|
|
826
|
+
fullReadoutMode: false,
|
|
773
827
|
};
|
|
774
828
|
|
|
775
829
|
setSessions(prev => {
|
|
@@ -789,7 +843,12 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
789
843
|
}, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, usageAllowed, refreshUsage]);
|
|
790
844
|
|
|
791
845
|
// Open the session panel (restore last session or create new)
|
|
792
|
-
const openSessionPanel = useCallback(() => {
|
|
846
|
+
const openSessionPanel = useCallback(async () => {
|
|
847
|
+
// If sessions haven't loaded yet (DB wasn't ready on mount), retry now
|
|
848
|
+
if (!sessionsLoadedRef.current) {
|
|
849
|
+
await loadSessions();
|
|
850
|
+
}
|
|
851
|
+
|
|
793
852
|
const savedSessionId = sessionStorage.getItem(ACTIVE_SESSION_KEY);
|
|
794
853
|
|
|
795
854
|
if (savedSessionId && sessions.has(savedSessionId)) {
|
|
@@ -807,10 +866,10 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
807
866
|
|
|
808
867
|
// No sessions exist - create new standalone session (unless over usage limit)
|
|
809
868
|
if (usageAllowed) {
|
|
810
|
-
createNewSession();
|
|
869
|
+
await createNewSession();
|
|
811
870
|
}
|
|
812
871
|
setClaudePanelOpen(true);
|
|
813
|
-
}, [sessions, switchSession, createNewSession, usageAllowed]);
|
|
872
|
+
}, [sessions, switchSession, createNewSession, usageAllowed, loadSessions]);
|
|
814
873
|
|
|
815
874
|
// Send message via the active session's stream manager
|
|
816
875
|
// With per-session streams, each session has its own manager - no cross-contamination possible
|
|
@@ -926,6 +985,31 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
926
985
|
});
|
|
927
986
|
}, [sessionRefs, registry]);
|
|
928
987
|
|
|
988
|
+
// Toggle full readout mode (raw stream-json events) for active session
|
|
989
|
+
const toggleFullReadout = useCallback(() => {
|
|
990
|
+
const currentActiveId = sessionRefs.activeSessionId.current;
|
|
991
|
+
if (!currentActiveId) return;
|
|
992
|
+
|
|
993
|
+
setSessions(prev => {
|
|
994
|
+
const session = prev.get(currentActiveId);
|
|
995
|
+
if (!session) return prev;
|
|
996
|
+
|
|
997
|
+
const newFullReadout = !session.fullReadoutMode;
|
|
998
|
+
|
|
999
|
+
const streamManager = registry.get(currentActiveId);
|
|
1000
|
+
if (streamManager) {
|
|
1001
|
+
streamManager.setFullReadoutModeQuiet(newFullReadout);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const updated = new Map(prev);
|
|
1005
|
+
updated.set(currentActiveId, {
|
|
1006
|
+
...session,
|
|
1007
|
+
fullReadoutMode: newFullReadout,
|
|
1008
|
+
});
|
|
1009
|
+
return updated;
|
|
1010
|
+
});
|
|
1011
|
+
}, [sessionRefs, registry]);
|
|
1012
|
+
|
|
929
1013
|
// Create an "Add to Backlog" session with initial assistant message
|
|
930
1014
|
// With per-session streams, each session has its own manager in registry
|
|
931
1015
|
const createAddToBacklogSession = useCallback(async () => {
|
|
@@ -934,25 +1018,12 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
934
1018
|
}
|
|
935
1019
|
|
|
936
1020
|
try {
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
body: JSON.stringify({ title: 'Add to Backlog' }),
|
|
1021
|
+
const sessionId = await invoke<number>('db_create_claude_session', {
|
|
1022
|
+
title: 'Add to Backlog',
|
|
1023
|
+
sessionTitle: null,
|
|
941
1024
|
});
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const errorData = await response.json();
|
|
945
|
-
if (errorData.code === 'SESSION_LIMIT_REACHED') {
|
|
946
|
-
console.warn(`[ClaudeSessionContext] ${errorData.error}`);
|
|
947
|
-
showToast('Session limit reached. Close existing sessions to create new ones.', 'error');
|
|
948
|
-
} else {
|
|
949
|
-
console.error('[ClaudeSessionContext] Failed to create backlog session:', errorData.error);
|
|
950
|
-
}
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
const { id, title } = await response.json();
|
|
955
|
-
// API returns string IDs
|
|
1025
|
+
const id = String(sessionId);
|
|
1026
|
+
const title = 'Add to Backlog';
|
|
956
1027
|
|
|
957
1028
|
setStandaloneSessions(prev => [...prev, {
|
|
958
1029
|
id,
|
|
@@ -970,7 +1041,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
970
1041
|
|
|
971
1042
|
// Create stream manager in registry
|
|
972
1043
|
const streamManager = getOrCreateStreamManager(id, true, handleWorkItemCreated);
|
|
973
|
-
registry.
|
|
1044
|
+
registry.recordActivity(id);
|
|
974
1045
|
// Initialize with the welcome message
|
|
975
1046
|
streamManager.setMessages([initialMessage]);
|
|
976
1047
|
|
|
@@ -983,6 +1054,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
983
1054
|
error: null,
|
|
984
1055
|
exitCode: null,
|
|
985
1056
|
narratedMode: true,
|
|
1057
|
+
fullReadoutMode: false,
|
|
986
1058
|
};
|
|
987
1059
|
|
|
988
1060
|
setSessions(prev => {
|
|
@@ -1004,22 +1076,12 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1004
1076
|
// Create a welcome session for blank projects with initial assistant message
|
|
1005
1077
|
const createWelcomeSession = useCallback(async () => {
|
|
1006
1078
|
try {
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
body: JSON.stringify({ title: 'Welcome' }),
|
|
1079
|
+
const sessionId = await invoke<number>('db_create_claude_session', {
|
|
1080
|
+
title: 'Welcome',
|
|
1081
|
+
sessionTitle: null,
|
|
1011
1082
|
});
|
|
1012
|
-
|
|
1013
|
-
|
|
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();
|
|
1083
|
+
const id = String(sessionId);
|
|
1084
|
+
const title = 'Welcome';
|
|
1023
1085
|
|
|
1024
1086
|
setStandaloneSessions(prev => [...prev, {
|
|
1025
1087
|
id,
|
|
@@ -1069,7 +1131,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1069
1131
|
const welcomeMessages = [greetingMessage, backlogTip, workflowTip, ctaMessage];
|
|
1070
1132
|
|
|
1071
1133
|
const streamManager = getOrCreateStreamManager(id, true, handleWorkItemCreated);
|
|
1072
|
-
registry.
|
|
1134
|
+
registry.recordActivity(id);
|
|
1073
1135
|
streamManager.setMessages(welcomeMessages);
|
|
1074
1136
|
|
|
1075
1137
|
const newSession: Session = {
|
|
@@ -1081,6 +1143,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1081
1143
|
error: null,
|
|
1082
1144
|
exitCode: null,
|
|
1083
1145
|
narratedMode: false,
|
|
1146
|
+
fullReadoutMode: false,
|
|
1084
1147
|
};
|
|
1085
1148
|
|
|
1086
1149
|
setSessions(prev => {
|
|
@@ -1096,6 +1159,70 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1096
1159
|
}
|
|
1097
1160
|
}, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId]);
|
|
1098
1161
|
|
|
1162
|
+
// Create a session that invokes a skill by name
|
|
1163
|
+
const createSkillSession = useCallback(async (skillName: string, title: string, customMessage?: string): Promise<string> => {
|
|
1164
|
+
if (!usageAllowed) {
|
|
1165
|
+
return '';
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
try {
|
|
1169
|
+
const sessionId = await invoke<number>('db_create_claude_session', {
|
|
1170
|
+
title,
|
|
1171
|
+
sessionTitle: null,
|
|
1172
|
+
});
|
|
1173
|
+
const id = String(sessionId);
|
|
1174
|
+
|
|
1175
|
+
setStandaloneSessions(prev => [...prev, {
|
|
1176
|
+
id,
|
|
1177
|
+
title,
|
|
1178
|
+
featureId: null,
|
|
1179
|
+
featureTitle: null,
|
|
1180
|
+
}]);
|
|
1181
|
+
|
|
1182
|
+
const userMessage: ClaudeMessage = {
|
|
1183
|
+
type: 'user',
|
|
1184
|
+
content: customMessage ?? `/${skillName} Go`,
|
|
1185
|
+
timestamp: Date.now(),
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
const streamManager = getOrCreateStreamManager(id, true, handleWorkItemCreated);
|
|
1189
|
+
registry.recordActivity(id);
|
|
1190
|
+
streamManager.setMessages([userMessage]);
|
|
1191
|
+
|
|
1192
|
+
const newSession: Session = {
|
|
1193
|
+
id,
|
|
1194
|
+
title,
|
|
1195
|
+
type: 'standalone',
|
|
1196
|
+
messages: [userMessage],
|
|
1197
|
+
status: 'idle',
|
|
1198
|
+
error: null,
|
|
1199
|
+
exitCode: null,
|
|
1200
|
+
narratedMode: true,
|
|
1201
|
+
fullReadoutMode: false,
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
setSessions(prev => {
|
|
1205
|
+
const updated = new Map(prev);
|
|
1206
|
+
updated.set(id, newSession);
|
|
1207
|
+
return updated;
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
setActiveSessionId(id);
|
|
1211
|
+
setClaudePanelOpen(true);
|
|
1212
|
+
|
|
1213
|
+
const machine = getStateMachine(id);
|
|
1214
|
+
machine.send('SEND');
|
|
1215
|
+
streamManager.sendMessage(userMessage.content!);
|
|
1216
|
+
|
|
1217
|
+
refreshUsage();
|
|
1218
|
+
return id;
|
|
1219
|
+
} catch (err) {
|
|
1220
|
+
console.error('[ClaudeSessionContext] Failed to create skill session:', err);
|
|
1221
|
+
showToast('Failed to create session. Please try again.', 'error');
|
|
1222
|
+
return '';
|
|
1223
|
+
}
|
|
1224
|
+
}, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, getStateMachine, usageAllowed, refreshUsage]);
|
|
1225
|
+
|
|
1099
1226
|
// Create a "Run Scenario" session with preloaded cucumber-js command
|
|
1100
1227
|
const createRunScenarioSession = useCallback(async (featureFile: string, scenarioTitle: string) => {
|
|
1101
1228
|
if (!usageAllowed) {
|
|
@@ -1104,25 +1231,12 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1104
1231
|
|
|
1105
1232
|
try {
|
|
1106
1233
|
const sessionTitle = `Run: ${scenarioTitle}`;
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
body: JSON.stringify({ title: sessionTitle }),
|
|
1234
|
+
const sessionId = await invoke<number>('db_create_claude_session', {
|
|
1235
|
+
title: sessionTitle,
|
|
1236
|
+
sessionTitle: null,
|
|
1111
1237
|
});
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
const errorData = await response.json();
|
|
1115
|
-
if (errorData.code === 'SESSION_LIMIT_REACHED') {
|
|
1116
|
-
console.warn(`[ClaudeSessionContext] ${errorData.error}`);
|
|
1117
|
-
showToast('Session limit reached. Close existing sessions to create new ones.', 'error');
|
|
1118
|
-
} else {
|
|
1119
|
-
console.error('[ClaudeSessionContext] Failed to create run scenario session:', errorData.error);
|
|
1120
|
-
showToast('Failed to create session. Please try again.', 'error');
|
|
1121
|
-
}
|
|
1122
|
-
return;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
const { id, title } = await response.json();
|
|
1238
|
+
const id = String(sessionId);
|
|
1239
|
+
const title = sessionTitle;
|
|
1126
1240
|
|
|
1127
1241
|
setStandaloneSessions(prev => [...prev, {
|
|
1128
1242
|
id,
|
|
@@ -1138,7 +1252,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1138
1252
|
};
|
|
1139
1253
|
|
|
1140
1254
|
const streamManager = getOrCreateStreamManager(id, true, handleWorkItemCreated);
|
|
1141
|
-
registry.
|
|
1255
|
+
registry.recordActivity(id);
|
|
1142
1256
|
streamManager.setMessages([userMessage]);
|
|
1143
1257
|
|
|
1144
1258
|
const newSession: Session = {
|
|
@@ -1150,6 +1264,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1150
1264
|
error: null,
|
|
1151
1265
|
exitCode: null,
|
|
1152
1266
|
narratedMode: true,
|
|
1267
|
+
fullReadoutMode: false,
|
|
1153
1268
|
};
|
|
1154
1269
|
|
|
1155
1270
|
setSessions(prev => {
|
|
@@ -1182,25 +1297,12 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1182
1297
|
|
|
1183
1298
|
try {
|
|
1184
1299
|
const sessionTitle = `Fix: ${scenarioTitle}`;
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
body: JSON.stringify({ title: sessionTitle }),
|
|
1300
|
+
const sessionId = await invoke<number>('db_create_claude_session', {
|
|
1301
|
+
title: sessionTitle,
|
|
1302
|
+
sessionTitle: null,
|
|
1189
1303
|
});
|
|
1190
|
-
|
|
1191
|
-
|
|
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();
|
|
1304
|
+
const id = String(sessionId);
|
|
1305
|
+
const title = sessionTitle;
|
|
1204
1306
|
|
|
1205
1307
|
setStandaloneSessions(prev => [...prev, {
|
|
1206
1308
|
id,
|
|
@@ -1236,7 +1338,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1236
1338
|
};
|
|
1237
1339
|
|
|
1238
1340
|
const streamManager = getOrCreateStreamManager(id, true, handleWorkItemCreated);
|
|
1239
|
-
registry.
|
|
1341
|
+
registry.recordActivity(id);
|
|
1240
1342
|
streamManager.setMessages([userMessage]);
|
|
1241
1343
|
|
|
1242
1344
|
const newSession: Session = {
|
|
@@ -1248,6 +1350,7 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1248
1350
|
error: null,
|
|
1249
1351
|
exitCode: null,
|
|
1250
1352
|
narratedMode: true,
|
|
1353
|
+
fullReadoutMode: false,
|
|
1251
1354
|
};
|
|
1252
1355
|
|
|
1253
1356
|
setSessions(prev => {
|
|
@@ -1272,6 +1375,150 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1272
1375
|
}
|
|
1273
1376
|
}, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, getStateMachine, usageAllowed, refreshUsage]);
|
|
1274
1377
|
|
|
1378
|
+
// Create a "Fix Service" session with context about crashed/degraded services
|
|
1379
|
+
const createFixServiceSession = useCallback(async (crashedServices: { name: string; port: number | null }[]) => {
|
|
1380
|
+
if (!usageAllowed) {
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
try {
|
|
1385
|
+
const serviceNames = crashedServices.map(s => s.name).join(', ');
|
|
1386
|
+
const sessionTitle = `Fix: Degraded Services`;
|
|
1387
|
+
const sessionId = await invoke<number>('db_create_claude_session', {
|
|
1388
|
+
title: sessionTitle,
|
|
1389
|
+
sessionTitle: null,
|
|
1390
|
+
});
|
|
1391
|
+
const id = String(sessionId);
|
|
1392
|
+
const title = sessionTitle;
|
|
1393
|
+
|
|
1394
|
+
setStandaloneSessions(prev => [...prev, {
|
|
1395
|
+
id,
|
|
1396
|
+
title,
|
|
1397
|
+
featureId: null,
|
|
1398
|
+
featureTitle: null,
|
|
1399
|
+
}]);
|
|
1400
|
+
|
|
1401
|
+
const promptParts = [
|
|
1402
|
+
'Services are degraded on the QA environment and need investigation.',
|
|
1403
|
+
'',
|
|
1404
|
+
`**Crashed services:** ${serviceNames}`,
|
|
1405
|
+
'',
|
|
1406
|
+
];
|
|
1407
|
+
|
|
1408
|
+
for (const svc of crashedServices) {
|
|
1409
|
+
promptParts.push(`- **${svc.name}** (port ${svc.port ?? 'unknown'}): crashed`);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
promptParts.push('', 'Please investigate why these services crashed and fix the issues.');
|
|
1413
|
+
|
|
1414
|
+
const userMessage: ClaudeMessage = {
|
|
1415
|
+
type: 'user',
|
|
1416
|
+
content: promptParts.join('\n'),
|
|
1417
|
+
timestamp: Date.now(),
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
const streamManager = getOrCreateStreamManager(id, true, handleWorkItemCreated);
|
|
1421
|
+
registry.recordActivity(id);
|
|
1422
|
+
streamManager.setMessages([userMessage]);
|
|
1423
|
+
|
|
1424
|
+
const newSession: Session = {
|
|
1425
|
+
id,
|
|
1426
|
+
title,
|
|
1427
|
+
type: 'standalone',
|
|
1428
|
+
messages: [userMessage],
|
|
1429
|
+
status: 'idle',
|
|
1430
|
+
error: null,
|
|
1431
|
+
exitCode: null,
|
|
1432
|
+
narratedMode: true,
|
|
1433
|
+
fullReadoutMode: false,
|
|
1434
|
+
};
|
|
1435
|
+
|
|
1436
|
+
setSessions(prev => {
|
|
1437
|
+
const updated = new Map(prev);
|
|
1438
|
+
updated.set(id, newSession);
|
|
1439
|
+
return updated;
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
setActiveSessionId(id);
|
|
1443
|
+
setClaudePanelOpen(true);
|
|
1444
|
+
|
|
1445
|
+
// Auto-send the fix request to Claude
|
|
1446
|
+
const machine = getStateMachine(id);
|
|
1447
|
+
machine.send('SEND');
|
|
1448
|
+
streamManager.sendMessage(userMessage.content!);
|
|
1449
|
+
|
|
1450
|
+
// Refresh usage so UI reflects the new session immediately
|
|
1451
|
+
refreshUsage();
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
console.error('[ClaudeSessionContext] Failed to create fix service session:', err);
|
|
1454
|
+
showToast('Failed to create session. Please try again.', 'error');
|
|
1455
|
+
}
|
|
1456
|
+
}, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, getStateMachine, usageAllowed, refreshUsage]);
|
|
1457
|
+
|
|
1458
|
+
// Create a "Set up BDD tests" session with the preflight setup prompt
|
|
1459
|
+
const createBddSetupSession = useCallback(async (setupPrompt: string) => {
|
|
1460
|
+
if (!usageAllowed) {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
try {
|
|
1465
|
+
const sessionTitle = 'Set up BDD tests';
|
|
1466
|
+
const sessionId = await invoke<number>('db_create_claude_session', {
|
|
1467
|
+
title: sessionTitle,
|
|
1468
|
+
sessionTitle: null,
|
|
1469
|
+
});
|
|
1470
|
+
const id = String(sessionId);
|
|
1471
|
+
const title = sessionTitle;
|
|
1472
|
+
|
|
1473
|
+
setStandaloneSessions(prev => [...prev, {
|
|
1474
|
+
id,
|
|
1475
|
+
title,
|
|
1476
|
+
featureId: null,
|
|
1477
|
+
featureTitle: null,
|
|
1478
|
+
}]);
|
|
1479
|
+
|
|
1480
|
+
const userMessage: ClaudeMessage = {
|
|
1481
|
+
type: 'user',
|
|
1482
|
+
content: setupPrompt,
|
|
1483
|
+
timestamp: Date.now(),
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
const streamManager = getOrCreateStreamManager(id, true, handleWorkItemCreated);
|
|
1487
|
+
registry.recordActivity(id);
|
|
1488
|
+
streamManager.setMessages([userMessage]);
|
|
1489
|
+
|
|
1490
|
+
const newSession: Session = {
|
|
1491
|
+
id,
|
|
1492
|
+
title,
|
|
1493
|
+
type: 'standalone',
|
|
1494
|
+
messages: [userMessage],
|
|
1495
|
+
status: 'idle',
|
|
1496
|
+
error: null,
|
|
1497
|
+
exitCode: null,
|
|
1498
|
+
narratedMode: true,
|
|
1499
|
+
fullReadoutMode: false,
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
setSessions(prev => {
|
|
1503
|
+
const updated = new Map(prev);
|
|
1504
|
+
updated.set(id, newSession);
|
|
1505
|
+
return updated;
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
setActiveSessionId(id);
|
|
1509
|
+
setClaudePanelOpen(true);
|
|
1510
|
+
|
|
1511
|
+
const machine = getStateMachine(id);
|
|
1512
|
+
machine.send('SEND');
|
|
1513
|
+
streamManager.sendMessage(userMessage.content!);
|
|
1514
|
+
|
|
1515
|
+
refreshUsage();
|
|
1516
|
+
} catch (err) {
|
|
1517
|
+
console.error('[ClaudeSessionContext] Failed to create BDD setup session:', err);
|
|
1518
|
+
showToast('Failed to create session. Please try again.', 'error');
|
|
1519
|
+
}
|
|
1520
|
+
}, [showToast, registry, getOrCreateStreamManager, handleWorkItemCreated, setActiveSessionId, getStateMachine, usageAllowed, refreshUsage]);
|
|
1521
|
+
|
|
1275
1522
|
// Setters for direct manipulation (e.g., restoring from DB)
|
|
1276
1523
|
// These now work through the registry
|
|
1277
1524
|
const setMessages = useCallback((messages: ClaudeMessage[]) => {
|
|
@@ -1312,6 +1559,8 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1312
1559
|
canRetry: activeStreamManager?.canRetry ?? false,
|
|
1313
1560
|
queuedMessage: activeStreamManager?.queuedMessage ?? null,
|
|
1314
1561
|
narratedMode: activeSession?.narratedMode ?? false,
|
|
1562
|
+
fullReadoutMode: activeSession?.fullReadoutMode ?? false,
|
|
1563
|
+
rawEvents: activeStreamManager?.rawEvents ?? [],
|
|
1315
1564
|
isTabSwitching,
|
|
1316
1565
|
}), [claudePanelOpen, sessions, activeSessionId, activeSession, standaloneSessions, activeStreamManager, isTabSwitching]);
|
|
1317
1566
|
|
|
@@ -1326,12 +1575,16 @@ export function ClaudeSessionProvider({ children }: { children: ReactNode }) {
|
|
|
1326
1575
|
createAddToBacklogSession,
|
|
1327
1576
|
createRunScenarioSession,
|
|
1328
1577
|
createFixScenarioSession,
|
|
1578
|
+
createFixServiceSession,
|
|
1579
|
+
createBddSetupSession,
|
|
1329
1580
|
createWelcomeSession,
|
|
1581
|
+
createSkillSession,
|
|
1330
1582
|
sendMessage,
|
|
1331
1583
|
retry,
|
|
1332
1584
|
stop,
|
|
1333
1585
|
toggleNarratedMode,
|
|
1334
|
-
|
|
1586
|
+
toggleFullReadout,
|
|
1587
|
+
}), [setClaudePanelOpen, openSession, switchSession, closeSession, openSessionPanel, createNewSession, createAddToBacklogSession, createRunScenarioSession, createFixScenarioSession, createFixServiceSession, createBddSetupSession, createWelcomeSession, createSkillSession, sendMessage, retry, stop, toggleNarratedMode, toggleFullReadout]);
|
|
1335
1588
|
|
|
1336
1589
|
// Memoize persistence context — stable setters
|
|
1337
1590
|
const persistenceValue: SessionPersistenceContextValue = useMemo(() => ({
|