@tanstack/ai-devtools-core 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +1 -0
  2. package/dist/esm/components/ConversationDetails.js +74 -1
  3. package/dist/esm/components/ConversationDetails.js.map +1 -1
  4. package/dist/esm/components/conversation/ActivityEventsTab.d.ts +8 -0
  5. package/dist/esm/components/conversation/ActivityEventsTab.js +72 -0
  6. package/dist/esm/components/conversation/ActivityEventsTab.js.map +1 -0
  7. package/dist/esm/components/conversation/ConversationHeader.js +109 -66
  8. package/dist/esm/components/conversation/ConversationHeader.js.map +1 -1
  9. package/dist/esm/components/conversation/ConversationTabs.d.ts +1 -1
  10. package/dist/esm/components/conversation/ConversationTabs.js +107 -3
  11. package/dist/esm/components/conversation/ConversationTabs.js.map +1 -1
  12. package/dist/esm/components/conversation/MessageCard.js +83 -28
  13. package/dist/esm/components/conversation/MessageCard.js.map +1 -1
  14. package/dist/esm/components/conversation/index.d.ts +1 -0
  15. package/dist/esm/store/ai-context.d.ts +19 -2
  16. package/dist/esm/store/ai-context.js +528 -262
  17. package/dist/esm/store/ai-context.js.map +1 -1
  18. package/dist/esm/styles/use-styles.d.ts +7 -0
  19. package/dist/esm/styles/use-styles.js +65 -1
  20. package/dist/esm/styles/use-styles.js.map +1 -1
  21. package/package.json +2 -2
  22. package/src/components/ConversationDetails.tsx +70 -1
  23. package/src/components/conversation/ActivityEventsTab.tsx +57 -0
  24. package/src/components/conversation/ConversationHeader.tsx +52 -36
  25. package/src/components/conversation/ConversationTabs.tsx +121 -2
  26. package/src/components/conversation/MessageCard.tsx +45 -10
  27. package/src/components/conversation/index.ts +1 -0
  28. package/src/store/ai-context.tsx +783 -418
  29. package/src/styles/use-styles.ts +65 -1
@@ -4,7 +4,7 @@ import { aiEventClient } from '@tanstack/ai/event-client'
4
4
  import type { ParentComponent } from 'solid-js'
5
5
 
6
6
  interface MessagePart {
7
- type: 'text' | 'tool-call' | 'tool-result'
7
+ type: 'text' | 'tool-call' | 'tool-result' | 'thinking'
8
8
  content?: string
9
9
  toolCallId?: string
10
10
  toolName?: string
@@ -84,6 +84,8 @@ export interface Chunk {
84
84
  duration?: number
85
85
  /** Number of raw chunks that were merged into this consolidated chunk */
86
86
  chunkCount: number
87
+ /** Whether this is a client-side tool execution */
88
+ isClientTool?: boolean
87
89
  }
88
90
 
89
91
  export interface SummarizeOperation {
@@ -96,6 +98,13 @@ export interface SummarizeOperation {
96
98
  status: 'started' | 'completed'
97
99
  }
98
100
 
101
+ export interface ActivityEvent {
102
+ id: string
103
+ name: string
104
+ timestamp: number
105
+ payload: unknown
106
+ }
107
+
99
108
  export interface Conversation {
100
109
  id: string
101
110
  type: 'client' | 'server'
@@ -111,12 +120,21 @@ export interface Conversation {
111
120
  iterationCount?: number
112
121
  toolNames?: Array<string>
113
122
  options?: Record<string, unknown>
114
- providerOptions?: Record<string, unknown>
123
+ modelOptions?: Record<string, unknown>
124
+ systemPrompts?: Array<string>
115
125
  /** Flags for which operation types this conversation has */
116
126
  hasChat?: boolean
117
127
  hasSummarize?: boolean
128
+ hasImage?: boolean
129
+ hasSpeech?: boolean
130
+ hasTranscription?: boolean
131
+ hasVideo?: boolean
118
132
  /** Summarize operations in this conversation */
119
133
  summaries?: Array<SummarizeOperation>
134
+ imageEvents?: Array<ActivityEvent>
135
+ speechEvents?: Array<ActivityEvent>
136
+ transcriptionEvents?: Array<ActivityEvent>
137
+ videoEvents?: Array<ActivityEvent>
120
138
  }
121
139
 
122
140
  interface AIStoreState {
@@ -336,6 +354,10 @@ export const AIProvider: ParentComponent = (props) => {
336
354
  label,
337
355
  messages: [],
338
356
  chunks: [],
357
+ imageEvents: [],
358
+ speechEvents: [],
359
+ transcriptionEvents: [],
360
+ videoEvents: [],
339
361
  status: 'active',
340
362
  startedAt: Date.now(),
341
363
  })
@@ -398,6 +420,9 @@ export const AIProvider: ParentComponent = (props) => {
398
420
  return
399
421
  }
400
422
  }
423
+
424
+ // Fallback: add to conversation's main chunks array if no assistant message found
425
+ addChunk(conversationId, chunk)
401
426
  }
402
427
 
403
428
  function updateMessageUsage(
@@ -553,6 +578,38 @@ export const AIProvider: ParentComponent = (props) => {
553
578
  queueChunk(conversationId, chunk)
554
579
  }
555
580
 
581
+ function addActivityEvent(
582
+ conversationId: string,
583
+ activity:
584
+ | 'imageEvents'
585
+ | 'speechEvents'
586
+ | 'transcriptionEvents'
587
+ | 'videoEvents',
588
+ event: ActivityEvent,
589
+ ): void {
590
+ if (!state.conversations[conversationId]) return
591
+
592
+ setState(
593
+ 'conversations',
594
+ conversationId,
595
+ activity,
596
+ produce((events: Array<ActivityEvent> | undefined) => {
597
+ if (!events) return [event]
598
+ events.push(event)
599
+ return events
600
+ }),
601
+ )
602
+
603
+ const activityFlagMap: Record<typeof activity, keyof Conversation> = {
604
+ imageEvents: 'hasImage',
605
+ speechEvents: 'hasSpeech',
606
+ transcriptionEvents: 'hasTranscription',
607
+ videoEvents: 'hasVideo',
608
+ }
609
+
610
+ setState('conversations', conversationId, activityFlagMap[activity], true)
611
+ }
612
+
556
613
  /**
557
614
  * For server conversations, ensure a message exists for the given messageId.
558
615
  * This creates a placeholder message that will be updated as chunks arrive.
@@ -598,58 +655,146 @@ export const AIProvider: ParentComponent = (props) => {
598
655
  }),
599
656
  )
600
657
 
658
+ // ============= Message Events =============
659
+
601
660
  cleanupFns.push(
602
- aiEventClient.on('client:message-sent', (e) => {
603
- const clientId = e.payload.clientId
604
- if (!state.conversations[clientId]) {
661
+ aiEventClient.on('text:message:created', (e) => {
662
+ const { clientId, streamId, messageId, role, content, timestamp } =
663
+ e.payload
664
+ const conversationId =
665
+ clientId ||
666
+ (streamId ? streamToConversation.get(streamId) : undefined)
667
+
668
+ if (!conversationId) return
669
+ if (role === 'tool' || role === 'system') return
670
+
671
+ if (!state.conversations[conversationId]) {
605
672
  getOrCreateConversation(
606
- clientId,
607
- 'client',
608
- `Client Chat (${clientId.substring(0, 8)})`,
673
+ conversationId,
674
+ clientId ? 'client' : 'server',
675
+ clientId
676
+ ? `Client Chat (${conversationId.substring(0, 8)})`
677
+ : `Server Chat (${conversationId.substring(0, 8)})`,
609
678
  )
610
679
  }
611
- addMessage(clientId, {
612
- id: e.payload.messageId,
613
- role: 'user',
614
- content: e.payload.content,
615
- timestamp: e.payload.timestamp,
616
- source: 'client',
680
+
681
+ const conv = state.conversations[conversationId]
682
+ if (!conv) return
683
+
684
+ const existingIndex = conv.messages.findIndex(
685
+ (message) => message.id === messageId,
686
+ )
687
+
688
+ const parts = e.payload.parts?.map((part) => {
689
+ if (part.type === 'text') {
690
+ return { type: 'text', content: part.content } satisfies MessagePart
691
+ }
692
+ if (part.type === 'tool-call') {
693
+ return {
694
+ type: 'tool-call',
695
+ toolCallId: part.id,
696
+ toolName: part.name,
697
+ arguments: part.arguments,
698
+ state: part.state,
699
+ output: part.output,
700
+ content: part.approval
701
+ ? JSON.stringify(part.approval)
702
+ : undefined,
703
+ } satisfies MessagePart
704
+ }
705
+ if (part.type === 'tool-result') {
706
+ return {
707
+ type: 'tool-result',
708
+ toolCallId: part.toolCallId,
709
+ content: part.content,
710
+ state: part.state,
711
+ error: part.error,
712
+ } satisfies MessagePart
713
+ }
714
+ return {
715
+ type: 'thinking',
716
+ content: part.content,
717
+ } satisfies MessagePart
617
718
  })
618
- updateConversation(clientId, { status: 'active' })
719
+
720
+ const toolCalls = e.payload.toolCalls?.map((toolCall) => ({
721
+ id: toolCall.id,
722
+ name: toolCall.function.name,
723
+ arguments: toolCall.function.arguments,
724
+ state: 'input-complete',
725
+ }))
726
+
727
+ const source = e.payload.source ?? (clientId ? 'client' : 'server')
728
+
729
+ if (role === 'user' && conv.type === 'client' && source === 'server') {
730
+ return
731
+ }
732
+
733
+ // Skip empty assistant messages from client - the real content comes from server
734
+ if (
735
+ role === 'assistant' &&
736
+ source === 'client' &&
737
+ !content &&
738
+ (!toolCalls || toolCalls.length === 0)
739
+ ) {
740
+ return
741
+ }
742
+
743
+ const messagePayload: Message = {
744
+ id: messageId,
745
+ role,
746
+ content,
747
+ timestamp,
748
+ parts,
749
+ toolCalls,
750
+ source,
751
+ }
752
+
753
+ if (existingIndex >= 0) {
754
+ updateMessage(conversationId, existingIndex, messagePayload)
755
+ } else {
756
+ addMessage(conversationId, messagePayload)
757
+ }
758
+
759
+ updateConversation(conversationId, { status: 'active', hasChat: true })
619
760
  }),
620
761
  )
621
762
 
622
763
  cleanupFns.push(
623
- aiEventClient.on('client:message-appended', (e) => {
624
- const clientId = e.payload.clientId
625
- const role = e.payload.role
764
+ aiEventClient.on('text:message:user', (e) => {
765
+ const { clientId, streamId, messageId, content, timestamp } = e.payload
766
+ const conversationId =
767
+ clientId ||
768
+ (streamId ? streamToConversation.get(streamId) : undefined)
769
+ if (!conversationId) return
626
770
 
627
- if (role === 'user') return
628
- if (!state.conversations[clientId]) return
771
+ const conv = state.conversations[conversationId]
772
+ if (!conv) return
629
773
 
630
- if (role === 'assistant') {
631
- addMessage(clientId, {
632
- id: e.payload.messageId,
633
- role: 'assistant',
634
- content: e.payload.contentPreview,
635
- timestamp: e.payload.timestamp,
636
- source: 'client',
637
- })
638
- } else if (role === 'tool') {
639
- // Tool result message from the LLM
640
- addMessage(clientId, {
641
- id: e.payload.messageId,
642
- role: 'tool',
643
- content: e.payload.contentPreview,
644
- timestamp: e.payload.timestamp,
645
- source: 'client',
646
- })
774
+ const existingIndex = conv.messages.findIndex(
775
+ (message) => message.id === messageId,
776
+ )
777
+
778
+ if (existingIndex >= 0) return
779
+
780
+ const source = e.payload.source ?? (clientId ? 'client' : 'server')
781
+
782
+ if (conv.type === 'client' && source === 'server') {
783
+ return
647
784
  }
785
+
786
+ addMessage(conversationId, {
787
+ id: messageId,
788
+ role: 'user',
789
+ content,
790
+ timestamp,
791
+ source,
792
+ })
648
793
  }),
649
794
  )
650
795
 
651
796
  cleanupFns.push(
652
- aiEventClient.on('client:loading-changed', (e) => {
797
+ aiEventClient.on('client:loading:changed', (e) => {
653
798
  const clientId = e.payload.clientId
654
799
  if (state.conversations[clientId]) {
655
800
  updateConversation(clientId, {
@@ -672,7 +817,7 @@ export const AIProvider: ParentComponent = (props) => {
672
817
  )
673
818
 
674
819
  cleanupFns.push(
675
- aiEventClient.on('client:messages-cleared', (e) => {
820
+ aiEventClient.on('client:messages:cleared', (e) => {
676
821
  const clientId = e.payload.clientId
677
822
  if (state.conversations[clientId]) {
678
823
  updateConversation(clientId, {
@@ -698,7 +843,7 @@ export const AIProvider: ParentComponent = (props) => {
698
843
  )
699
844
 
700
845
  cleanupFns.push(
701
- aiEventClient.on('client:error-changed', (e) => {
846
+ aiEventClient.on('client:error:changed', (e) => {
702
847
  const clientId = e.payload.clientId
703
848
  if (state.conversations[clientId] && e.payload.error) {
704
849
  updateConversation(clientId, { status: 'error' })
@@ -706,112 +851,10 @@ export const AIProvider: ParentComponent = (props) => {
706
851
  }),
707
852
  )
708
853
 
709
- cleanupFns.push(
710
- aiEventClient.on('client:assistant-message-updated', (e) => {
711
- const clientId = e.payload.clientId
712
- const messageId = e.payload.messageId
713
- const content = e.payload.content
714
-
715
- if (!state.conversations[clientId]) return
716
-
717
- const conv = state.conversations[clientId]
718
-
719
- // Find message by ID anywhere in the list, not just the last one
720
- const messageIndex = conv.messages.findIndex(
721
- (m: Message) => m.id === messageId,
722
- )
723
-
724
- // Only update existing messages, don't create new ones
725
- // (client:message-appended is responsible for creating messages)
726
- if (messageIndex >= 0) {
727
- updateMessage(clientId, messageIndex, {
728
- content,
729
- model: conv.model,
730
- })
731
- }
732
- }),
733
- )
734
-
735
- cleanupFns.push(
736
- aiEventClient.on('client:tool-call-updated', (e) => {
737
- const {
738
- clientId,
739
- messageId,
740
- toolCallId,
741
- toolName,
742
- state: toolCallState,
743
- arguments: args,
744
- } = e.payload as {
745
- clientId: string
746
- messageId: string
747
- toolCallId: string
748
- toolName: string
749
- state: string
750
- arguments: unknown
751
- timestamp: number
752
- }
753
-
754
- if (!state.conversations[clientId]) return
755
-
756
- const conv = state.conversations[clientId]
757
- const messageIndex = conv.messages.findIndex(
758
- (m: Message) => m.id === messageId,
759
- )
760
- if (messageIndex === -1) return
761
-
762
- const message = conv.messages[messageIndex]
763
- if (!message) return
764
-
765
- const toolCalls = message.toolCalls || []
766
- const existingToolIndex = toolCalls.findIndex(
767
- (t: ToolCall) => t.id === toolCallId,
768
- )
769
-
770
- const toolCall: ToolCall = {
771
- id: toolCallId,
772
- name: toolName,
773
- arguments: JSON.stringify(args, null, 2),
774
- state: toolCallState,
775
- }
776
-
777
- if (existingToolIndex >= 0) {
778
- updateToolCall(clientId, messageIndex, existingToolIndex, toolCall)
779
- } else {
780
- setToolCalls(clientId, messageIndex, [...toolCalls, toolCall])
781
- }
782
- }),
783
- )
784
-
785
- cleanupFns.push(
786
- aiEventClient.on('client:approval-requested', (e) => {
787
- const { clientId, messageId, toolCallId, approvalId } = e.payload
788
-
789
- if (!state.conversations[clientId]) return
790
-
791
- const conv = state.conversations[clientId]
792
- const messageIndex = conv.messages.findIndex((m) => m.id === messageId)
793
- if (messageIndex === -1) return
794
-
795
- const message = conv.messages[messageIndex]
796
- if (!message?.toolCalls) return
797
-
798
- const toolCallIndex = message.toolCalls.findIndex(
799
- (t) => t.id === toolCallId,
800
- )
801
- if (toolCallIndex === -1) return
802
-
803
- updateToolCall(clientId, messageIndex, toolCallIndex, {
804
- approvalRequired: true,
805
- approvalId,
806
- state: 'approval-requested',
807
- })
808
- }),
809
- )
810
-
811
854
  // ============= Tool Events =============
812
855
 
813
856
  cleanupFns.push(
814
- aiEventClient.on('tool:result-added', (e) => {
857
+ aiEventClient.on('tools:result:added', (e) => {
815
858
  const {
816
859
  clientId,
817
860
  toolCallId,
@@ -821,10 +864,23 @@ export const AIProvider: ParentComponent = (props) => {
821
864
  timestamp,
822
865
  } = e.payload
823
866
 
867
+ if (!clientId) return
824
868
  if (!state.conversations[clientId]) return
825
869
 
826
870
  const conv = state.conversations[clientId]
827
871
 
872
+ // Always create a chunk for the tool result
873
+ const chunk: Chunk = {
874
+ id: `chunk-tool-result-${toolCallId}-${Date.now()}`,
875
+ type: 'tool_result',
876
+ toolCallId,
877
+ toolName,
878
+ result: output,
879
+ timestamp,
880
+ chunkCount: 1,
881
+ isClientTool: true,
882
+ }
883
+
828
884
  // Find the message with the tool call and update it
829
885
  for (
830
886
  let messageIndex = conv.messages.length - 1;
@@ -844,28 +900,25 @@ export const AIProvider: ParentComponent = (props) => {
844
900
  state: resultState === 'output-error' ? 'error' : 'complete',
845
901
  })
846
902
 
847
- // Also add a chunk to show the tool result in the chunks view
848
- const chunk: Chunk = {
849
- id: `chunk-tool-result-${toolCallId}-${Date.now()}`,
850
- type: 'tool_result',
851
- messageId: message.id,
852
- toolCallId,
853
- toolName,
854
- result: output,
855
- timestamp,
856
- chunkCount: 1,
857
- }
903
+ // Add chunk with message ID
904
+ chunk.messageId = message.id
858
905
  addChunkToMessage(clientId, chunk)
859
906
  return
860
907
  }
861
908
  }
909
+
910
+ // If we couldn't find the message with toolCalls, still add the chunk
911
+ // This handles cases where the message hasn't been processed yet
912
+ // or the tool call is a client-only tool
913
+ addChunkToMessage(clientId, chunk)
862
914
  }),
863
915
  )
864
916
 
865
917
  cleanupFns.push(
866
- aiEventClient.on('tool:approval-responded', (e) => {
918
+ aiEventClient.on('tools:approval:responded', (e) => {
867
919
  const { clientId, toolCallId, approved } = e.payload
868
920
 
921
+ if (!clientId) return
869
922
  if (!state.conversations[clientId]) return
870
923
 
871
924
  const conv = state.conversations[clientId]
@@ -892,52 +945,82 @@ export const AIProvider: ParentComponent = (props) => {
892
945
  }),
893
946
  )
894
947
 
895
- // ============= Stream Events =============
896
-
897
948
  cleanupFns.push(
898
- aiEventClient.on('stream:chunk:content', (e) => {
899
- const streamId = e.payload.streamId
900
- const conversationId = streamToConversation.get(streamId)
901
- if (!conversationId) return
949
+ aiEventClient.on('tools:call:updated', (e) => {
950
+ const {
951
+ clientId,
952
+ streamId,
953
+ messageId,
954
+ toolCallId,
955
+ toolName,
956
+ state: toolCallState,
957
+ arguments: args,
958
+ } = e.payload
902
959
 
903
- const chunk: Chunk = {
904
- id: `chunk-${Date.now()}-${Math.random()}`,
905
- type: 'content',
906
- messageId: e.payload.messageId,
907
- content: e.payload.content,
908
- delta: e.payload.delta,
909
- timestamp: e.payload.timestamp,
910
- chunkCount: 1,
911
- }
960
+ const conversationId =
961
+ clientId ||
962
+ (streamId ? streamToConversation.get(streamId) : undefined)
963
+ if (!conversationId || !state.conversations[conversationId]) return
912
964
 
913
965
  const conv = state.conversations[conversationId]
914
- if (conv?.type === 'client') {
915
- addChunkToMessage(conversationId, chunk)
916
- } else {
917
- ensureMessageForChunk(
966
+ const messageIndex = conv.messages.findIndex(
967
+ (m: Message) => m.id === messageId,
968
+ )
969
+ if (messageIndex === -1) return
970
+
971
+ const message = conv.messages[messageIndex]
972
+ if (!message) return
973
+
974
+ const toolCalls = message.toolCalls || []
975
+ const existingToolIndex = toolCalls.findIndex(
976
+ (t: ToolCall) => t.id === toolCallId,
977
+ )
978
+
979
+ const toolCall: ToolCall = {
980
+ id: toolCallId,
981
+ name: toolName,
982
+ arguments: args,
983
+ state: toolCallState,
984
+ }
985
+
986
+ if (existingToolIndex >= 0) {
987
+ updateToolCall(
918
988
  conversationId,
919
- e.payload.messageId,
920
- e.payload.timestamp,
989
+ messageIndex,
990
+ existingToolIndex,
991
+ toolCall,
921
992
  )
922
- addChunk(conversationId, chunk)
993
+ } else {
994
+ setToolCalls(conversationId, messageIndex, [...toolCalls, toolCall])
923
995
  }
924
996
  }),
925
997
  )
926
998
 
927
999
  cleanupFns.push(
928
- aiEventClient.on('stream:chunk:tool-call', (e) => {
929
- const streamId = e.payload.streamId
930
- const conversationId = streamToConversation.get(streamId)
1000
+ aiEventClient.on('tools:input:available', (e) => {
1001
+ const {
1002
+ streamId,
1003
+ messageId,
1004
+ toolCallId,
1005
+ toolName,
1006
+ input,
1007
+ timestamp,
1008
+ clientId,
1009
+ } = e.payload
1010
+
1011
+ const conversationId =
1012
+ clientId ||
1013
+ (streamId ? streamToConversation.get(streamId) : undefined)
931
1014
  if (!conversationId) return
932
1015
 
933
1016
  const chunk: Chunk = {
934
1017
  id: `chunk-${Date.now()}-${Math.random()}`,
935
1018
  type: 'tool_call',
936
- messageId: e.payload.messageId,
937
- toolCallId: e.payload.toolCallId,
938
- toolName: e.payload.toolName,
939
- arguments: e.payload.arguments,
940
- timestamp: e.payload.timestamp,
1019
+ messageId: messageId,
1020
+ toolCallId,
1021
+ toolName,
1022
+ arguments: JSON.stringify(input),
1023
+ timestamp,
941
1024
  chunkCount: 1,
942
1025
  }
943
1026
 
@@ -945,36 +1028,209 @@ export const AIProvider: ParentComponent = (props) => {
945
1028
  if (conv?.type === 'client') {
946
1029
  addChunkToMessage(conversationId, chunk)
947
1030
  } else {
948
- ensureMessageForChunk(
949
- conversationId,
950
- e.payload.messageId,
951
- e.payload.timestamp,
952
- )
953
1031
  addChunk(conversationId, chunk)
954
1032
  }
955
1033
  }),
956
1034
  )
957
1035
 
958
1036
  cleanupFns.push(
959
- aiEventClient.on('stream:chunk:tool-result', (e) => {
960
- const streamId = e.payload.streamId
961
- const conversationId = streamToConversation.get(streamId)
962
- if (!conversationId) return
1037
+ aiEventClient.on('tools:call:completed', (e) => {
1038
+ const {
1039
+ streamId,
1040
+ toolCallId,
1041
+ toolName,
1042
+ result,
1043
+ duration,
1044
+ messageId,
1045
+ timestamp,
1046
+ clientId,
1047
+ } = e.payload
963
1048
 
964
- const chunk: Chunk = {
965
- id: `chunk-${Date.now()}-${Math.random()}`,
966
- type: 'tool_result',
967
- messageId: e.payload.messageId,
968
- toolCallId: e.payload.toolCallId,
969
- content: e.payload.result,
970
- timestamp: e.payload.timestamp,
971
- chunkCount: 1,
972
- }
1049
+ const conversationId =
1050
+ clientId ||
1051
+ (streamId ? streamToConversation.get(streamId) : undefined)
1052
+ if (!conversationId || !state.conversations[conversationId]) return
973
1053
 
974
1054
  const conv = state.conversations[conversationId]
975
- if (conv?.type === 'client') {
976
- addChunkToMessage(conversationId, chunk)
977
- } else {
1055
+
1056
+ const chunk: Chunk = {
1057
+ id: `chunk-tool-result-${toolCallId}-${Date.now()}`,
1058
+ type: 'tool_result',
1059
+ messageId: messageId,
1060
+ toolCallId,
1061
+ toolName,
1062
+ result,
1063
+ duration,
1064
+ timestamp,
1065
+ chunkCount: 1,
1066
+ }
1067
+
1068
+ if (conv.type === 'client' && messageId) {
1069
+ const messageIndex = conv.messages.findIndex(
1070
+ (m) => m.id === messageId,
1071
+ )
1072
+ if (messageIndex !== -1) {
1073
+ queueMessageChunk(conversationId, messageIndex, chunk)
1074
+ } else {
1075
+ for (let i = conv.messages.length - 1; i >= 0; i--) {
1076
+ if (conv.messages[i]?.role === 'assistant') {
1077
+ queueMessageChunk(conversationId, i, chunk)
1078
+ break
1079
+ }
1080
+ }
1081
+ }
1082
+ } else {
1083
+ addChunk(conversationId, chunk)
1084
+ }
1085
+
1086
+ for (let i = conv.messages.length - 1; i >= 0; i--) {
1087
+ const message = conv.messages[i]
1088
+ if (!message?.toolCalls) continue
1089
+
1090
+ const toolCallIndex = message.toolCalls.findIndex(
1091
+ (t) => t.id === toolCallId,
1092
+ )
1093
+ if (toolCallIndex >= 0) {
1094
+ updateToolCall(conversationId, i, toolCallIndex, {
1095
+ duration,
1096
+ result,
1097
+ })
1098
+ return
1099
+ }
1100
+ }
1101
+ }),
1102
+ )
1103
+
1104
+ // ============= Stream Events =============
1105
+
1106
+ cleanupFns.push(
1107
+ aiEventClient.on('text:chunk:content', (e) => {
1108
+ const streamId = e.payload.streamId
1109
+ const conversationId = streamToConversation.get(streamId)
1110
+ if (!conversationId) return
1111
+
1112
+ const chunk: Chunk = {
1113
+ id: `chunk-${Date.now()}-${Math.random()}`,
1114
+ type: 'content',
1115
+ messageId: e.payload.messageId,
1116
+ content: e.payload.content,
1117
+ delta: e.payload.delta,
1118
+ timestamp: e.payload.timestamp,
1119
+ chunkCount: 1,
1120
+ }
1121
+
1122
+ const conv = state.conversations[conversationId]
1123
+ if (conv?.type === 'client') {
1124
+ addChunkToMessage(conversationId, chunk)
1125
+ } else {
1126
+ ensureMessageForChunk(
1127
+ conversationId,
1128
+ e.payload.messageId,
1129
+ e.payload.timestamp,
1130
+ )
1131
+ addChunk(conversationId, chunk)
1132
+ }
1133
+
1134
+ if (e.payload.messageId) {
1135
+ const messageIndex = conv?.messages.findIndex(
1136
+ (msg) => msg.id === e.payload.messageId,
1137
+ )
1138
+ if (messageIndex !== undefined && messageIndex >= 0) {
1139
+ updateMessage(conversationId, messageIndex, {
1140
+ content: e.payload.content,
1141
+ })
1142
+ }
1143
+ }
1144
+ }),
1145
+ )
1146
+
1147
+ cleanupFns.push(
1148
+ aiEventClient.on('text:chunk:tool-call', (e) => {
1149
+ const streamId = e.payload.streamId
1150
+ const conversationId = streamToConversation.get(streamId)
1151
+ if (!conversationId) return
1152
+
1153
+ const chunk: Chunk = {
1154
+ id: `chunk-${Date.now()}-${Math.random()}`,
1155
+ type: 'tool_call',
1156
+ messageId: e.payload.messageId,
1157
+ toolCallId: e.payload.toolCallId,
1158
+ toolName: e.payload.toolName,
1159
+ arguments: e.payload.arguments,
1160
+ timestamp: e.payload.timestamp,
1161
+ chunkCount: 1,
1162
+ }
1163
+
1164
+ const conv = state.conversations[conversationId]
1165
+ if (conv?.type === 'client') {
1166
+ addChunkToMessage(conversationId, chunk)
1167
+ } else {
1168
+ ensureMessageForChunk(
1169
+ conversationId,
1170
+ e.payload.messageId,
1171
+ e.payload.timestamp,
1172
+ )
1173
+ addChunk(conversationId, chunk)
1174
+ }
1175
+
1176
+ if (e.payload.messageId) {
1177
+ const messageIndex = conv?.messages.findIndex(
1178
+ (msg) => msg.id === e.payload.messageId,
1179
+ )
1180
+ if (messageIndex !== undefined && messageIndex >= 0) {
1181
+ const message = conv?.messages[messageIndex]
1182
+ if (!message) return
1183
+
1184
+ const toolCalls = message.toolCalls || []
1185
+ const existingToolIndex = toolCalls.findIndex(
1186
+ (t: ToolCall) => t.id === e.payload.toolCallId,
1187
+ )
1188
+
1189
+ const toolCall: ToolCall = {
1190
+ id: e.payload.toolCallId,
1191
+ name: e.payload.toolName,
1192
+ arguments: e.payload.arguments,
1193
+ state: 'input-streaming',
1194
+ }
1195
+
1196
+ if (existingToolIndex >= 0) {
1197
+ updateToolCall(
1198
+ conversationId,
1199
+ messageIndex,
1200
+ existingToolIndex,
1201
+ toolCall,
1202
+ )
1203
+ } else {
1204
+ setToolCalls(conversationId, messageIndex, [
1205
+ ...toolCalls,
1206
+ toolCall,
1207
+ ])
1208
+ }
1209
+ }
1210
+ }
1211
+ }),
1212
+ )
1213
+
1214
+ cleanupFns.push(
1215
+ aiEventClient.on('text:chunk:tool-result', (e) => {
1216
+ const streamId = e.payload.streamId
1217
+ const conversationId = streamToConversation.get(streamId)
1218
+ if (!conversationId) return
1219
+
1220
+ const chunk: Chunk = {
1221
+ id: `chunk-${Date.now()}-${Math.random()}`,
1222
+ type: 'tool_result',
1223
+ messageId: e.payload.messageId,
1224
+ toolCallId: e.payload.toolCallId,
1225
+ result: e.payload.result,
1226
+ timestamp: e.payload.timestamp,
1227
+ chunkCount: 1,
1228
+ }
1229
+
1230
+ const conv = state.conversations[conversationId]
1231
+ if (conv?.type === 'client') {
1232
+ addChunkToMessage(conversationId, chunk)
1233
+ } else {
978
1234
  ensureMessageForChunk(
979
1235
  conversationId,
980
1236
  e.payload.messageId,
@@ -1005,7 +1261,7 @@ export const AIProvider: ParentComponent = (props) => {
1005
1261
  )
1006
1262
 
1007
1263
  cleanupFns.push(
1008
- aiEventClient.on('stream:chunk:thinking', (e) => {
1264
+ aiEventClient.on('text:chunk:thinking', (e) => {
1009
1265
  const streamId = e.payload.streamId
1010
1266
  const conversationId = streamToConversation.get(streamId)
1011
1267
  if (!conversationId) return
@@ -1046,7 +1302,7 @@ export const AIProvider: ParentComponent = (props) => {
1046
1302
  )
1047
1303
 
1048
1304
  cleanupFns.push(
1049
- aiEventClient.on('stream:chunk:done', (e) => {
1305
+ aiEventClient.on('text:chunk:done', (e) => {
1050
1306
  const streamId = e.payload.streamId
1051
1307
  const conversationId = streamToConversation.get(streamId)
1052
1308
  if (!conversationId) return
@@ -1089,7 +1345,7 @@ export const AIProvider: ParentComponent = (props) => {
1089
1345
  )
1090
1346
 
1091
1347
  cleanupFns.push(
1092
- aiEventClient.on('stream:chunk:error', (e) => {
1348
+ aiEventClient.on('text:chunk:error', (e) => {
1093
1349
  const streamId = e.payload.streamId
1094
1350
  const conversationId = streamToConversation.get(streamId)
1095
1351
  if (!conversationId) return
@@ -1123,20 +1379,7 @@ export const AIProvider: ParentComponent = (props) => {
1123
1379
  )
1124
1380
 
1125
1381
  cleanupFns.push(
1126
- aiEventClient.on('stream:ended', (e) => {
1127
- const streamId = e.payload.streamId
1128
- const conversationId = streamToConversation.get(streamId)
1129
- if (!conversationId) return
1130
-
1131
- updateConversation(conversationId, {
1132
- status: 'completed',
1133
- completedAt: e.payload.timestamp,
1134
- })
1135
- }),
1136
- )
1137
-
1138
- cleanupFns.push(
1139
- aiEventClient.on('stream:approval-requested', (e) => {
1382
+ aiEventClient.on('tools:approval:requested', (e) => {
1140
1383
  const {
1141
1384
  streamId,
1142
1385
  messageId,
@@ -1145,9 +1388,11 @@ export const AIProvider: ParentComponent = (props) => {
1145
1388
  input,
1146
1389
  approvalId,
1147
1390
  timestamp,
1391
+ clientId,
1148
1392
  } = e.payload
1149
1393
 
1150
- const conversationId = streamToConversation.get(streamId)
1394
+ const conversationId =
1395
+ clientId || streamToConversation.get(streamId) || ''
1151
1396
  if (!conversationId) return
1152
1397
 
1153
1398
  const conv = state.conversations[conversationId]
@@ -1192,112 +1437,10 @@ export const AIProvider: ParentComponent = (props) => {
1192
1437
  }),
1193
1438
  )
1194
1439
 
1195
- // ============= Processor Events =============
1196
-
1197
- cleanupFns.push(
1198
- aiEventClient.on('processor:text-updated', (e) => {
1199
- const streamId = e.payload.streamId
1200
-
1201
- let conversationId = streamToConversation.get(streamId)
1202
-
1203
- if (!conversationId) {
1204
- const activeClients = Object.values(state.conversations)
1205
- .filter((c) => c.type === 'client' && c.status === 'active')
1206
- .sort((a, b) => b.startedAt - a.startedAt)
1207
-
1208
- if (activeClients.length > 0 && activeClients[0]) {
1209
- conversationId = activeClients[0].id
1210
- streamToConversation.set(streamId, conversationId)
1211
- }
1212
- }
1213
-
1214
- if (!conversationId) return
1215
-
1216
- const conv = state.conversations[conversationId]
1217
- if (!conv) return
1218
-
1219
- // Only update existing assistant messages, don't create new ones
1220
- // (client:message-appended is responsible for creating messages)
1221
- const lastMessage = conv.messages[conv.messages.length - 1]
1222
- if (lastMessage && lastMessage.role === 'assistant') {
1223
- updateMessage(conversationId, conv.messages.length - 1, {
1224
- content: e.payload.content,
1225
- })
1226
- }
1227
- }),
1228
- )
1229
-
1230
- cleanupFns.push(
1231
- aiEventClient.on('processor:tool-call-state-changed', (e) => {
1232
- const streamId = e.payload.streamId
1233
- const conversationId = streamToConversation.get(streamId)
1234
-
1235
- if (!conversationId || !state.conversations[conversationId]) return
1236
-
1237
- const conv = state.conversations[conversationId]
1238
- const lastMessage = conv.messages[conv.messages.length - 1]
1239
-
1240
- if (lastMessage && lastMessage.role === 'assistant') {
1241
- const toolCalls = lastMessage.toolCalls || []
1242
- const existingToolIndex = toolCalls.findIndex(
1243
- (t) => t.id === e.payload.toolCallId,
1244
- )
1245
-
1246
- const toolCall: ToolCall = {
1247
- id: e.payload.toolCallId,
1248
- name: e.payload.toolName,
1249
- arguments: JSON.stringify(e.payload.arguments, null, 2),
1250
- state: e.payload.state,
1251
- }
1252
-
1253
- if (existingToolIndex >= 0) {
1254
- updateToolCall(
1255
- conversationId,
1256
- conv.messages.length - 1,
1257
- existingToolIndex,
1258
- toolCall,
1259
- )
1260
- } else {
1261
- setToolCalls(conversationId, conv.messages.length - 1, [
1262
- ...toolCalls,
1263
- toolCall,
1264
- ])
1265
- }
1266
- }
1267
- }),
1268
- )
1269
-
1270
- cleanupFns.push(
1271
- aiEventClient.on('processor:tool-result-state-changed', (e) => {
1272
- const streamId = e.payload.streamId
1273
- const conversationId = streamToConversation.get(streamId)
1274
-
1275
- if (!conversationId || !state.conversations[conversationId]) return
1276
-
1277
- const conv = state.conversations[conversationId]
1278
-
1279
- for (let i = conv.messages.length - 1; i >= 0; i--) {
1280
- const message = conv.messages[i]
1281
- if (!message?.toolCalls) continue
1282
-
1283
- const toolCallIndex = message.toolCalls.findIndex(
1284
- (t) => t.id === e.payload.toolCallId,
1285
- )
1286
- if (toolCallIndex >= 0) {
1287
- updateToolCall(conversationId, i, toolCallIndex, {
1288
- result: e.payload.content,
1289
- state: e.payload.error ? 'error' : e.payload.state,
1290
- })
1291
- return
1292
- }
1293
- }
1294
- }),
1295
- )
1296
-
1297
1440
  // ============= Chat Events (for usage tracking) =============
1298
1441
 
1299
1442
  cleanupFns.push(
1300
- aiEventClient.on('text:started', (e) => {
1443
+ aiEventClient.on('text:request:started', (e) => {
1301
1444
  const streamId = e.payload.streamId
1302
1445
  const model = e.payload.model
1303
1446
  const provider = e.payload.provider
@@ -1305,7 +1448,13 @@ export const AIProvider: ParentComponent = (props) => {
1305
1448
 
1306
1449
  if (clientId && state.conversations[clientId]) {
1307
1450
  streamToConversation.set(streamId, clientId)
1308
- updateConversation(clientId, { status: 'active', ...e.payload })
1451
+ requestToConversation.set(e.payload.requestId, clientId)
1452
+ updateConversation(clientId, {
1453
+ status: 'active',
1454
+ ...e.payload,
1455
+ systemPrompts: e.payload.systemPrompts,
1456
+ hasChat: true,
1457
+ })
1309
1458
  return
1310
1459
  }
1311
1460
 
@@ -1315,7 +1464,12 @@ export const AIProvider: ParentComponent = (props) => {
1315
1464
 
1316
1465
  if (activeClient) {
1317
1466
  streamToConversation.set(streamId, activeClient.id)
1318
- updateConversation(activeClient.id, { ...e.payload })
1467
+ requestToConversation.set(e.payload.requestId, activeClient.id)
1468
+ updateConversation(activeClient.id, {
1469
+ ...e.payload,
1470
+ systemPrompts: e.payload.systemPrompts,
1471
+ hasChat: true,
1472
+ })
1319
1473
  } else {
1320
1474
  const existingServerConv = Object.values(state.conversations).find(
1321
1475
  (c) => c.type === 'server' && c.model === model,
@@ -1323,22 +1477,33 @@ export const AIProvider: ParentComponent = (props) => {
1323
1477
 
1324
1478
  if (existingServerConv) {
1325
1479
  streamToConversation.set(streamId, existingServerConv.id)
1480
+ requestToConversation.set(
1481
+ e.payload.requestId,
1482
+ existingServerConv.id,
1483
+ )
1326
1484
  updateConversation(existingServerConv.id, {
1327
1485
  status: 'active',
1328
1486
  ...e.payload,
1487
+ systemPrompts: e.payload.systemPrompts,
1488
+ hasChat: true,
1329
1489
  })
1330
1490
  } else {
1331
1491
  const serverId = `server-${model}`
1332
1492
  getOrCreateConversation(serverId, 'server', `${model} Server`)
1333
1493
  streamToConversation.set(streamId, serverId)
1334
- updateConversation(serverId, { ...e.payload })
1494
+ requestToConversation.set(e.payload.requestId, serverId)
1495
+ updateConversation(serverId, {
1496
+ ...e.payload,
1497
+ systemPrompts: e.payload.systemPrompts,
1498
+ hasChat: true,
1499
+ })
1335
1500
  }
1336
1501
  }
1337
1502
  }),
1338
1503
  )
1339
1504
 
1340
1505
  cleanupFns.push(
1341
- aiEventClient.on('text:completed', (e) => {
1506
+ aiEventClient.on('text:request:completed', (e) => {
1342
1507
  const { requestId, usage } = e.payload
1343
1508
 
1344
1509
  const conversationId = requestToConversation.get(requestId)
@@ -1359,20 +1524,7 @@ export const AIProvider: ParentComponent = (props) => {
1359
1524
  )
1360
1525
 
1361
1526
  cleanupFns.push(
1362
- aiEventClient.on('text:iteration', (e) => {
1363
- const { requestId, iterationNumber } = e.payload
1364
-
1365
- const conversationId = requestToConversation.get(requestId)
1366
- if (conversationId && state.conversations[conversationId]) {
1367
- updateConversation(conversationId, {
1368
- iterationCount: iterationNumber,
1369
- })
1370
- }
1371
- }),
1372
- )
1373
-
1374
- cleanupFns.push(
1375
- aiEventClient.on('usage:tokens', (e) => {
1527
+ aiEventClient.on('text:usage', (e) => {
1376
1528
  const { requestId, usage, messageId } = e.payload
1377
1529
 
1378
1530
  const conversationId = requestToConversation.get(requestId)
@@ -1383,81 +1535,10 @@ export const AIProvider: ParentComponent = (props) => {
1383
1535
  }),
1384
1536
  )
1385
1537
 
1386
- // ============= Tool Call Completed (with duration) =============
1387
-
1388
- cleanupFns.push(
1389
- aiEventClient.on('tool:call-completed', (e) => {
1390
- const {
1391
- streamId,
1392
- toolCallId,
1393
- toolName,
1394
- result,
1395
- duration,
1396
- messageId,
1397
- timestamp,
1398
- } = e.payload
1399
-
1400
- const conversationId = streamToConversation.get(streamId)
1401
- if (!conversationId || !state.conversations[conversationId]) return
1402
-
1403
- const conv = state.conversations[conversationId]
1404
-
1405
- // Add a tool_result chunk to show the result in the chunks view
1406
- const chunk: Chunk = {
1407
- id: `chunk-tool-result-${toolCallId}-${Date.now()}`,
1408
- type: 'tool_result',
1409
- messageId: messageId,
1410
- toolCallId,
1411
- toolName,
1412
- result,
1413
- duration,
1414
- timestamp,
1415
- chunkCount: 1,
1416
- }
1417
-
1418
- // Add chunk to message if it's a client conversation, otherwise to conversation
1419
- if (conv.type === 'client' && messageId) {
1420
- const messageIndex = conv.messages.findIndex(
1421
- (m) => m.id === messageId,
1422
- )
1423
- if (messageIndex !== -1) {
1424
- queueMessageChunk(conversationId, messageIndex, chunk)
1425
- } else {
1426
- // If message not found, add to last assistant message
1427
- for (let i = conv.messages.length - 1; i >= 0; i--) {
1428
- if (conv.messages[i]?.role === 'assistant') {
1429
- queueMessageChunk(conversationId, i, chunk)
1430
- break
1431
- }
1432
- }
1433
- }
1434
- } else {
1435
- addChunk(conversationId, chunk)
1436
- }
1437
-
1438
- // Update the tool call with duration
1439
- for (let i = conv.messages.length - 1; i >= 0; i--) {
1440
- const message = conv.messages[i]
1441
- if (!message?.toolCalls) continue
1442
-
1443
- const toolCallIndex = message.toolCalls.findIndex(
1444
- (t) => t.id === toolCallId,
1445
- )
1446
- if (toolCallIndex >= 0) {
1447
- updateToolCall(conversationId, i, toolCallIndex, {
1448
- duration,
1449
- result,
1450
- })
1451
- return
1452
- }
1453
- }
1454
- }),
1455
- )
1456
-
1457
1538
  // ============= Summarize Events =============
1458
1539
 
1459
1540
  cleanupFns.push(
1460
- aiEventClient.on('summarize:started', (e) => {
1541
+ aiEventClient.on('summarize:request:started', (e) => {
1461
1542
  const { requestId, model, inputLength, timestamp, clientId } = e.payload
1462
1543
 
1463
1544
  // Try to find an active conversation to attach to, or create a new one
@@ -1505,7 +1586,7 @@ export const AIProvider: ParentComponent = (props) => {
1505
1586
  )
1506
1587
 
1507
1588
  cleanupFns.push(
1508
- aiEventClient.on('summarize:completed', (e) => {
1589
+ aiEventClient.on('summarize:request:completed', (e) => {
1509
1590
  const { requestId, outputLength, duration } = e.payload
1510
1591
 
1511
1592
  const conversationId = requestToConversation.get(requestId)
@@ -1533,6 +1614,290 @@ export const AIProvider: ParentComponent = (props) => {
1533
1614
  }),
1534
1615
  )
1535
1616
 
1617
+ // ============= Image Events =============
1618
+
1619
+ cleanupFns.push(
1620
+ aiEventClient.on('image:request:started', (e) => {
1621
+ const { requestId, clientId, timestamp } = e.payload
1622
+
1623
+ let conversationId = clientId
1624
+ if (!conversationId || !state.conversations[conversationId]) {
1625
+ conversationId = `image-${requestId}`
1626
+ getOrCreateConversation(
1627
+ conversationId,
1628
+ 'server',
1629
+ `Image (${requestId.substring(0, 8)})`,
1630
+ )
1631
+ }
1632
+
1633
+ addActivityEvent(conversationId, 'imageEvents', {
1634
+ id: requestId,
1635
+ name: 'image:request:started',
1636
+ timestamp,
1637
+ payload: e.payload,
1638
+ })
1639
+ }),
1640
+ )
1641
+
1642
+ cleanupFns.push(
1643
+ aiEventClient.on('image:request:completed', (e) => {
1644
+ const { requestId, clientId, timestamp } = e.payload
1645
+
1646
+ let conversationId = clientId
1647
+ if (!conversationId || !state.conversations[conversationId]) {
1648
+ conversationId = `image-${requestId}`
1649
+ getOrCreateConversation(
1650
+ conversationId,
1651
+ 'server',
1652
+ `Image (${requestId.substring(0, 8)})`,
1653
+ )
1654
+ }
1655
+
1656
+ addActivityEvent(conversationId, 'imageEvents', {
1657
+ id: requestId,
1658
+ name: 'image:request:completed',
1659
+ timestamp,
1660
+ payload: e.payload,
1661
+ })
1662
+ }),
1663
+ )
1664
+
1665
+ cleanupFns.push(
1666
+ aiEventClient.on('image:usage', (e) => {
1667
+ const { requestId, clientId, timestamp } = e.payload
1668
+
1669
+ let conversationId = clientId
1670
+ if (!conversationId || !state.conversations[conversationId]) {
1671
+ conversationId = `image-${requestId}`
1672
+ getOrCreateConversation(
1673
+ conversationId,
1674
+ 'server',
1675
+ `Image (${requestId.substring(0, 8)})`,
1676
+ )
1677
+ }
1678
+
1679
+ addActivityEvent(conversationId, 'imageEvents', {
1680
+ id: requestId,
1681
+ name: 'image:usage',
1682
+ timestamp,
1683
+ payload: e.payload,
1684
+ })
1685
+ }),
1686
+ )
1687
+
1688
+ // ============= Speech Events =============
1689
+
1690
+ cleanupFns.push(
1691
+ aiEventClient.on('speech:request:started', (e) => {
1692
+ const { requestId, clientId, timestamp } = e.payload
1693
+
1694
+ let conversationId = clientId
1695
+ if (!conversationId || !state.conversations[conversationId]) {
1696
+ conversationId = `speech-${requestId}`
1697
+ getOrCreateConversation(
1698
+ conversationId,
1699
+ 'server',
1700
+ `Speech (${requestId.substring(0, 8)})`,
1701
+ )
1702
+ }
1703
+
1704
+ addActivityEvent(conversationId, 'speechEvents', {
1705
+ id: requestId,
1706
+ name: 'speech:request:started',
1707
+ timestamp,
1708
+ payload: e.payload,
1709
+ })
1710
+ }),
1711
+ )
1712
+
1713
+ cleanupFns.push(
1714
+ aiEventClient.on('speech:request:completed', (e) => {
1715
+ const { requestId, clientId, timestamp } = e.payload
1716
+
1717
+ let conversationId = clientId
1718
+ if (!conversationId || !state.conversations[conversationId]) {
1719
+ conversationId = `speech-${requestId}`
1720
+ getOrCreateConversation(
1721
+ conversationId,
1722
+ 'server',
1723
+ `Speech (${requestId.substring(0, 8)})`,
1724
+ )
1725
+ }
1726
+
1727
+ addActivityEvent(conversationId, 'speechEvents', {
1728
+ id: requestId,
1729
+ name: 'speech:request:completed',
1730
+ timestamp,
1731
+ payload: e.payload,
1732
+ })
1733
+ }),
1734
+ )
1735
+
1736
+ cleanupFns.push(
1737
+ aiEventClient.on('speech:usage', (e) => {
1738
+ const { requestId, clientId, timestamp } = e.payload
1739
+
1740
+ let conversationId = clientId
1741
+ if (!conversationId || !state.conversations[conversationId]) {
1742
+ conversationId = `speech-${requestId}`
1743
+ getOrCreateConversation(
1744
+ conversationId,
1745
+ 'server',
1746
+ `Speech (${requestId.substring(0, 8)})`,
1747
+ )
1748
+ }
1749
+
1750
+ addActivityEvent(conversationId, 'speechEvents', {
1751
+ id: requestId,
1752
+ name: 'speech:usage',
1753
+ timestamp,
1754
+ payload: e.payload,
1755
+ })
1756
+ }),
1757
+ )
1758
+
1759
+ // ============= Transcription Events =============
1760
+
1761
+ cleanupFns.push(
1762
+ aiEventClient.on('transcription:request:started', (e) => {
1763
+ const { requestId, clientId, timestamp } = e.payload
1764
+
1765
+ let conversationId = clientId
1766
+ if (!conversationId || !state.conversations[conversationId]) {
1767
+ conversationId = `transcription-${requestId}`
1768
+ getOrCreateConversation(
1769
+ conversationId,
1770
+ 'server',
1771
+ `Transcription (${requestId.substring(0, 8)})`,
1772
+ )
1773
+ }
1774
+
1775
+ addActivityEvent(conversationId, 'transcriptionEvents', {
1776
+ id: requestId,
1777
+ name: 'transcription:request:started',
1778
+ timestamp,
1779
+ payload: e.payload,
1780
+ })
1781
+ }),
1782
+ )
1783
+
1784
+ cleanupFns.push(
1785
+ aiEventClient.on('transcription:request:completed', (e) => {
1786
+ const { requestId, clientId, timestamp } = e.payload
1787
+
1788
+ let conversationId = clientId
1789
+ if (!conversationId || !state.conversations[conversationId]) {
1790
+ conversationId = `transcription-${requestId}`
1791
+ getOrCreateConversation(
1792
+ conversationId,
1793
+ 'server',
1794
+ `Transcription (${requestId.substring(0, 8)})`,
1795
+ )
1796
+ }
1797
+
1798
+ addActivityEvent(conversationId, 'transcriptionEvents', {
1799
+ id: requestId,
1800
+ name: 'transcription:request:completed',
1801
+ timestamp,
1802
+ payload: e.payload,
1803
+ })
1804
+ }),
1805
+ )
1806
+
1807
+ cleanupFns.push(
1808
+ aiEventClient.on('transcription:usage', (e) => {
1809
+ const { requestId, clientId, timestamp } = e.payload
1810
+
1811
+ let conversationId = clientId
1812
+ if (!conversationId || !state.conversations[conversationId]) {
1813
+ conversationId = `transcription-${requestId}`
1814
+ getOrCreateConversation(
1815
+ conversationId,
1816
+ 'server',
1817
+ `Transcription (${requestId.substring(0, 8)})`,
1818
+ )
1819
+ }
1820
+
1821
+ addActivityEvent(conversationId, 'transcriptionEvents', {
1822
+ id: requestId,
1823
+ name: 'transcription:usage',
1824
+ timestamp,
1825
+ payload: e.payload,
1826
+ })
1827
+ }),
1828
+ )
1829
+
1830
+ // ============= Video Events =============
1831
+
1832
+ cleanupFns.push(
1833
+ aiEventClient.on('video:request:started', (e) => {
1834
+ const { requestId, clientId, timestamp } = e.payload
1835
+
1836
+ let conversationId = clientId
1837
+ if (!conversationId || !state.conversations[conversationId]) {
1838
+ conversationId = `video-${requestId}`
1839
+ getOrCreateConversation(
1840
+ conversationId,
1841
+ 'server',
1842
+ `Video (${requestId.substring(0, 8)})`,
1843
+ )
1844
+ }
1845
+
1846
+ addActivityEvent(conversationId, 'videoEvents', {
1847
+ id: requestId,
1848
+ name: 'video:request:started',
1849
+ timestamp,
1850
+ payload: e.payload,
1851
+ })
1852
+ }),
1853
+ )
1854
+
1855
+ cleanupFns.push(
1856
+ aiEventClient.on('video:request:completed', (e) => {
1857
+ const { requestId, clientId, timestamp } = e.payload
1858
+
1859
+ let conversationId = clientId
1860
+ if (!conversationId || !state.conversations[conversationId]) {
1861
+ conversationId = `video-${requestId}`
1862
+ getOrCreateConversation(
1863
+ conversationId,
1864
+ 'server',
1865
+ `Video (${requestId.substring(0, 8)})`,
1866
+ )
1867
+ }
1868
+
1869
+ addActivityEvent(conversationId, 'videoEvents', {
1870
+ id: requestId,
1871
+ name: 'video:request:completed',
1872
+ timestamp,
1873
+ payload: e.payload,
1874
+ })
1875
+ }),
1876
+ )
1877
+
1878
+ cleanupFns.push(
1879
+ aiEventClient.on('video:usage', (e) => {
1880
+ const { requestId, clientId, timestamp } = e.payload
1881
+
1882
+ let conversationId = clientId
1883
+ if (!conversationId || !state.conversations[conversationId]) {
1884
+ conversationId = `video-${requestId}`
1885
+ getOrCreateConversation(
1886
+ conversationId,
1887
+ 'server',
1888
+ `Video (${requestId.substring(0, 8)})`,
1889
+ )
1890
+ }
1891
+
1892
+ addActivityEvent(conversationId, 'videoEvents', {
1893
+ id: requestId,
1894
+ name: 'video:usage',
1895
+ timestamp,
1896
+ payload: e.payload,
1897
+ })
1898
+ }),
1899
+ )
1900
+
1536
1901
  // Cleanup all listeners on unmount
1537
1902
  onCleanup(() => {
1538
1903
  for (const cleanup of cleanupFns) {