@@ -28,7 +28,7 @@ import { SplitPane } from "@/components/ui/split-pane";
28
28
import { WebviewPreview } from "./WebviewPreview" ;
29
29
import type { ClaudeStreamMessage } from "./AgentExecution" ;
30
30
import { useVirtualizer } from "@tanstack/react-virtual" ;
31
- import { useTrackEvent , useComponentMetrics , useWorkflowTracking } from "@/hooks" ;
31
+ import { useTrackEvent , useComponentMetrics , useWorkflowTracking , useScreenReaderAnnouncements } from "@/hooks" ;
32
32
import { SessionPersistenceService } from "@/services/sessionPersistence" ;
33
33
34
34
interface ClaudeCodeSessionProps {
@@ -139,6 +139,15 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
139
139
// const aiTracking = useAIInteractionTracking('sonnet'); // Default model
140
140
const workflowTracking = useWorkflowTracking ( 'claude_session' ) ;
141
141
142
+ // Screen reader announcements
143
+ const {
144
+ announceClaudeStarted,
145
+ announceClaudeFinished,
146
+ announceAssistantMessage,
147
+ announceToolExecution,
148
+ announceToolCompleted
149
+ } = useScreenReaderAnnouncements ( ) ;
150
+
142
151
// Call onProjectPathChange when component mounts with initial path
143
152
useEffect ( ( ) => {
144
153
if ( onProjectPathChange && projectPath ) {
@@ -454,6 +463,9 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
454
463
setError ( null ) ;
455
464
hasActiveSessionRef . current = true ;
456
465
466
+ // Announce that Claude is starting to process
467
+ announceClaudeStarted ( ) ;
468
+
457
469
// For resuming sessions, ensure we have the session ID
458
470
if ( effectiveSession && ! claudeSessionId ) {
459
471
setClaudeSessionId ( effectiveSession . id ) ;
@@ -544,6 +556,60 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
544
556
}
545
557
} ) ;
546
558
559
+ // Helper to find tool name by tool use ID from previous messages
560
+ function findToolNameById ( toolUseId : string ) : string | null {
561
+ // Search backwards through messages for the tool use
562
+ for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
563
+ const msg = messages [ i ] ;
564
+ if ( msg . type === 'assistant' && msg . message ?. content ) {
565
+ const toolUse = msg . message . content . find ( ( c : any ) =>
566
+ c . type === 'tool_use' && c . id === toolUseId
567
+ ) ;
568
+ if ( toolUse ?. name ) {
569
+ return toolUse . name ;
570
+ }
571
+ }
572
+ }
573
+ return null ;
574
+ }
575
+
576
+ // Helper to announce incoming messages to screen readers
577
+ function announceIncomingMessage ( message : ClaudeStreamMessage ) {
578
+ if ( message . type === 'assistant' && message . message ?. content ) {
579
+ // Announce tool execution
580
+ const toolUses = message . message . content . filter ( ( c : any ) => c . type === 'tool_use' ) ;
581
+ toolUses . forEach ( ( toolUse : any ) => {
582
+ const toolName = toolUse . name || 'unknown tool' ;
583
+ const description = toolUse . input ?. description ||
584
+ toolUse . input ?. command ||
585
+ toolUse . input ?. file_path ||
586
+ toolUse . input ?. pattern ||
587
+ toolUse . input ?. prompt ?. substring ( 0 , 50 ) ;
588
+ announceToolExecution ( toolName , description ) ;
589
+ } ) ;
590
+
591
+ // Announce text content
592
+ const textContent = message . message . content
593
+ . filter ( ( c : any ) => c . type === 'text' )
594
+ . map ( ( c : any ) => typeof c . text === 'string' ? c . text : ( c . text ?. text || '' ) )
595
+ . join ( ' ' )
596
+ . trim ( ) ;
597
+
598
+ if ( textContent ) {
599
+ announceAssistantMessage ( textContent ) ;
600
+ }
601
+ } else if ( message . type === 'system' ) {
602
+ // Announce system messages if they have meaningful content
603
+ if ( message . subtype === 'init' ) {
604
+ // Don't announce init messages as they're just setup
605
+ return ;
606
+ } else if ( message . result || message . error ) {
607
+ const content = message . result || message . error || 'System message received' ;
608
+ announceAssistantMessage ( content ) ;
609
+ }
610
+ }
611
+ }
612
+
547
613
// Helper to process any JSONL stream message string
548
614
function handleStreamMessage ( payload : string ) {
549
615
try {
@@ -555,6 +621,9 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
555
621
556
622
const message = JSON . parse ( payload ) as ClaudeStreamMessage ;
557
623
624
+ // Announce incoming messages to screen readers
625
+ announceIncomingMessage ( message ) ;
626
+
558
627
// Track enhanced tool execution
559
628
if ( message . type === 'assistant' && message . message ?. content ) {
560
629
const toolUses = message . message . content . filter ( ( c : any ) => c . type === 'tool_use' ) ;
@@ -583,6 +652,14 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
583
652
const toolResults = message . message . content . filter ( ( c : any ) => c . type === 'tool_result' ) ;
584
653
toolResults . forEach ( ( result : any ) => {
585
654
const isError = result . is_error || false ;
655
+
656
+ // Announce tool completion
657
+ if ( result . tool_use_id ) {
658
+ // Try to find the tool name from previous messages
659
+ const toolName = findToolNameById ( result . tool_use_id ) || 'Tool' ;
660
+ // announceToolCompleted(toolName, !isError); // Disabled to prevent interrupting other announcements
661
+ }
662
+
586
663
// Note: We don't have execution time here, but we can track success/failure
587
664
if ( isError ) {
588
665
sessionMetrics . current . toolsFailed += 1 ;
@@ -634,6 +711,9 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
634
711
hasActiveSessionRef . current = false ;
635
712
isListeningRef . current = false ; // Reset listening state
636
713
714
+ // Announce that Claude has finished
715
+ announceClaudeFinished ( ) ;
716
+
637
717
// Track enhanced session stopped metrics when session completes
638
718
if ( effectiveSession && claudeSessionId ) {
639
719
const sessionStartTimeValue = messages . length > 0 ? messages [ 0 ] . timestamp || Date . now ( ) : Date . now ( ) ;
0 commit comments