@tanstack/ai-devtools-core 0.0.2 → 0.1.0

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.
@@ -86,15 +86,6 @@ export interface Chunk {
86
86
  chunkCount: number
87
87
  }
88
88
 
89
- export interface EmbeddingOperation {
90
- id: string
91
- model: string
92
- inputCount: number
93
- duration: number
94
- timestamp: number
95
- status: 'started' | 'completed'
96
- }
97
-
98
89
  export interface SummarizeOperation {
99
90
  id: string
100
91
  model: string
@@ -123,10 +114,7 @@ export interface Conversation {
123
114
  providerOptions?: Record<string, unknown>
124
115
  /** Flags for which operation types this conversation has */
125
116
  hasChat?: boolean
126
- hasEmbedding?: boolean
127
117
  hasSummarize?: boolean
128
- /** Embedding operations in this conversation */
129
- embeddings?: Array<EmbeddingOperation>
130
118
  /** Summarize operations in this conversation */
131
119
  summaries?: Array<SummarizeOperation>
132
120
  }
@@ -599,1165 +587,950 @@ export const AIProvider: ParentComponent = (props) => {
599
587
  // ============= Client Events =============
600
588
 
601
589
  cleanupFns.push(
602
- aiEventClient.on(
603
- 'client:created',
604
- (e) => {
605
- const clientId = e.payload.clientId
590
+ aiEventClient.on('client:created', (e) => {
591
+ const clientId = e.payload.clientId
592
+ getOrCreateConversation(
593
+ clientId,
594
+ 'client',
595
+ `Client Chat (${clientId.substring(0, 8)})`,
596
+ )
597
+ updateConversation(clientId, { model: undefined, provider: 'Client' })
598
+ }),
599
+ )
600
+
601
+ cleanupFns.push(
602
+ aiEventClient.on('client:message-sent', (e) => {
603
+ const clientId = e.payload.clientId
604
+ if (!state.conversations[clientId]) {
606
605
  getOrCreateConversation(
607
606
  clientId,
608
607
  'client',
609
608
  `Client Chat (${clientId.substring(0, 8)})`,
610
609
  )
611
- updateConversation(clientId, { model: undefined, provider: 'Client' })
612
- },
613
- { withEventTarget: false },
614
- ),
610
+ }
611
+ addMessage(clientId, {
612
+ id: e.payload.messageId,
613
+ role: 'user',
614
+ content: e.payload.content,
615
+ timestamp: e.payload.timestamp,
616
+ source: 'client',
617
+ })
618
+ updateConversation(clientId, { status: 'active' })
619
+ }),
615
620
  )
616
621
 
617
622
  cleanupFns.push(
618
- aiEventClient.on(
619
- 'client:message-sent',
620
- (e) => {
621
- const clientId = e.payload.clientId
622
- if (!state.conversations[clientId]) {
623
- getOrCreateConversation(
624
- clientId,
625
- 'client',
626
- `Client Chat (${clientId.substring(0, 8)})`,
627
- )
628
- }
623
+ aiEventClient.on('client:message-appended', (e) => {
624
+ const clientId = e.payload.clientId
625
+ const role = e.payload.role
626
+
627
+ if (role === 'user') return
628
+ if (!state.conversations[clientId]) return
629
+
630
+ if (role === 'assistant') {
629
631
  addMessage(clientId, {
630
632
  id: e.payload.messageId,
631
- role: 'user',
632
- content: e.payload.content,
633
+ role: 'assistant',
634
+ content: e.payload.contentPreview,
633
635
  timestamp: e.payload.timestamp,
634
636
  source: 'client',
635
637
  })
636
- updateConversation(clientId, { status: 'active' })
637
- },
638
- { withEventTarget: false },
639
- ),
640
- )
641
-
642
- cleanupFns.push(
643
- aiEventClient.on(
644
- 'client:message-appended',
645
- (e) => {
646
- const clientId = e.payload.clientId
647
- const role = e.payload.role
648
-
649
- if (role === 'user') return
650
- if (!state.conversations[clientId]) return
651
-
652
- if (role === 'assistant') {
653
- addMessage(clientId, {
654
- id: e.payload.messageId,
655
- role: 'assistant',
656
- content: e.payload.contentPreview,
657
- timestamp: e.payload.timestamp,
658
- source: 'client',
659
- })
660
- } else if (role === 'tool') {
661
- // Tool result message from the LLM
662
- addMessage(clientId, {
663
- id: e.payload.messageId,
664
- role: 'tool',
665
- content: e.payload.contentPreview,
666
- timestamp: e.payload.timestamp,
667
- source: 'client',
668
- })
669
- }
670
- },
671
- { withEventTarget: false },
672
- ),
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
+ })
647
+ }
648
+ }),
673
649
  )
674
650
 
675
651
  cleanupFns.push(
676
- aiEventClient.on(
677
- 'client:loading-changed',
678
- (e) => {
679
- const clientId = e.payload.clientId
680
- if (state.conversations[clientId]) {
681
- updateConversation(clientId, {
682
- status: e.payload.isLoading ? 'active' : 'completed',
683
- })
684
- }
685
- },
686
- { withEventTarget: false },
687
- ),
652
+ aiEventClient.on('client:loading-changed', (e) => {
653
+ const clientId = e.payload.clientId
654
+ if (state.conversations[clientId]) {
655
+ updateConversation(clientId, {
656
+ status: e.payload.isLoading ? 'active' : 'completed',
657
+ })
658
+ }
659
+ }),
688
660
  )
689
661
 
690
662
  cleanupFns.push(
691
- aiEventClient.on(
692
- 'client:stopped',
693
- (e) => {
694
- const clientId = e.payload.clientId
695
- if (state.conversations[clientId]) {
696
- updateConversation(clientId, {
697
- status: 'completed',
698
- completedAt: e.payload.timestamp,
699
- })
700
- }
701
- },
702
- { withEventTarget: false },
703
- ),
663
+ aiEventClient.on('client:stopped', (e) => {
664
+ const clientId = e.payload.clientId
665
+ if (state.conversations[clientId]) {
666
+ updateConversation(clientId, {
667
+ status: 'completed',
668
+ completedAt: e.payload.timestamp,
669
+ })
670
+ }
671
+ }),
704
672
  )
705
673
 
706
674
  cleanupFns.push(
707
- aiEventClient.on(
708
- 'client:messages-cleared',
709
- (e) => {
710
- const clientId = e.payload.clientId
711
- if (state.conversations[clientId]) {
712
- updateConversation(clientId, {
713
- messages: [],
714
- chunks: [],
715
- usage: undefined,
716
- })
717
- }
718
- },
719
- { withEventTarget: false },
720
- ),
675
+ aiEventClient.on('client:messages-cleared', (e) => {
676
+ const clientId = e.payload.clientId
677
+ if (state.conversations[clientId]) {
678
+ updateConversation(clientId, {
679
+ messages: [],
680
+ chunks: [],
681
+ usage: undefined,
682
+ })
683
+ }
684
+ }),
721
685
  )
722
686
 
723
687
  cleanupFns.push(
724
- aiEventClient.on(
725
- 'client:reloaded',
726
- (e) => {
727
- const clientId = e.payload.clientId
728
- const conv = state.conversations[clientId]
729
- if (conv) {
730
- updateConversation(clientId, {
731
- messages: conv.messages.slice(0, e.payload.fromMessageIndex),
732
- status: 'active',
733
- })
734
- }
735
- },
736
- { withEventTarget: false },
737
- ),
688
+ aiEventClient.on('client:reloaded', (e) => {
689
+ const clientId = e.payload.clientId
690
+ const conv = state.conversations[clientId]
691
+ if (conv) {
692
+ updateConversation(clientId, {
693
+ messages: conv.messages.slice(0, e.payload.fromMessageIndex),
694
+ status: 'active',
695
+ })
696
+ }
697
+ }),
738
698
  )
739
699
 
740
700
  cleanupFns.push(
741
- aiEventClient.on(
742
- 'client:error-changed',
743
- (e) => {
744
- const clientId = e.payload.clientId
745
- if (state.conversations[clientId] && e.payload.error) {
746
- updateConversation(clientId, { status: 'error' })
747
- }
748
- },
749
- { withEventTarget: false },
750
- ),
701
+ aiEventClient.on('client:error-changed', (e) => {
702
+ const clientId = e.payload.clientId
703
+ if (state.conversations[clientId] && e.payload.error) {
704
+ updateConversation(clientId, { status: 'error' })
705
+ }
706
+ }),
751
707
  )
752
708
 
753
709
  cleanupFns.push(
754
- aiEventClient.on(
755
- 'client:assistant-message-updated',
756
- (e) => {
757
- const clientId = e.payload.clientId
758
- const messageId = e.payload.messageId
759
- const content = e.payload.content
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
760
714
 
761
- if (!state.conversations[clientId]) return
715
+ if (!state.conversations[clientId]) return
762
716
 
763
- const conv = state.conversations[clientId]
717
+ const conv = state.conversations[clientId]
764
718
 
765
- // Find message by ID anywhere in the list, not just the last one
766
- const messageIndex = conv.messages.findIndex(
767
- (m: Message) => m.id === messageId,
768
- )
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
+ )
769
723
 
770
- // Only update existing messages, don't create new ones
771
- // (client:message-appended is responsible for creating messages)
772
- if (messageIndex >= 0) {
773
- updateMessage(clientId, messageIndex, {
774
- content,
775
- model: conv.model,
776
- })
777
- }
778
- },
779
- { withEventTarget: false },
780
- ),
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
+ }),
781
733
  )
782
734
 
783
735
  cleanupFns.push(
784
- aiEventClient.on(
785
- 'client:tool-call-updated',
786
- (e) => {
787
- const {
788
- clientId,
789
- messageId,
790
- toolCallId,
791
- toolName,
792
- state: toolCallState,
793
- arguments: args,
794
- } = e.payload as {
795
- clientId: string
796
- messageId: string
797
- toolCallId: string
798
- toolName: string
799
- state: string
800
- arguments: unknown
801
- timestamp: number
802
- }
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
+ }
803
753
 
804
- if (!state.conversations[clientId]) return
754
+ if (!state.conversations[clientId]) return
805
755
 
806
- const conv = state.conversations[clientId]
807
- const messageIndex = conv.messages.findIndex(
808
- (m: Message) => m.id === messageId,
809
- )
810
- if (messageIndex === -1) return
756
+ const conv = state.conversations[clientId]
757
+ const messageIndex = conv.messages.findIndex(
758
+ (m: Message) => m.id === messageId,
759
+ )
760
+ if (messageIndex === -1) return
811
761
 
812
- const message = conv.messages[messageIndex]
813
- if (!message) return
762
+ const message = conv.messages[messageIndex]
763
+ if (!message) return
814
764
 
815
- const toolCalls = message.toolCalls || []
816
- const existingToolIndex = toolCalls.findIndex(
817
- (t: ToolCall) => t.id === toolCallId,
818
- )
765
+ const toolCalls = message.toolCalls || []
766
+ const existingToolIndex = toolCalls.findIndex(
767
+ (t: ToolCall) => t.id === toolCallId,
768
+ )
819
769
 
820
- const toolCall: ToolCall = {
821
- id: toolCallId,
822
- name: toolName,
823
- arguments: JSON.stringify(args, null, 2),
824
- state: toolCallState,
825
- }
770
+ const toolCall: ToolCall = {
771
+ id: toolCallId,
772
+ name: toolName,
773
+ arguments: JSON.stringify(args, null, 2),
774
+ state: toolCallState,
775
+ }
826
776
 
827
- if (existingToolIndex >= 0) {
828
- updateToolCall(clientId, messageIndex, existingToolIndex, toolCall)
829
- } else {
830
- setToolCalls(clientId, messageIndex, [...toolCalls, toolCall])
831
- }
832
- },
833
- { withEventTarget: false },
834
- ),
777
+ if (existingToolIndex >= 0) {
778
+ updateToolCall(clientId, messageIndex, existingToolIndex, toolCall)
779
+ } else {
780
+ setToolCalls(clientId, messageIndex, [...toolCalls, toolCall])
781
+ }
782
+ }),
835
783
  )
836
784
 
837
785
  cleanupFns.push(
838
- aiEventClient.on(
839
- 'client:approval-requested',
840
- (e) => {
841
- const { clientId, messageId, toolCallId, approvalId } = e.payload
786
+ aiEventClient.on('client:approval-requested', (e) => {
787
+ const { clientId, messageId, toolCallId, approvalId } = e.payload
842
788
 
843
- if (!state.conversations[clientId]) return
789
+ if (!state.conversations[clientId]) return
844
790
 
845
- const conv = state.conversations[clientId]
846
- const messageIndex = conv.messages.findIndex(
847
- (m) => m.id === messageId,
848
- )
849
- if (messageIndex === -1) return
850
-
851
- const message = conv.messages[messageIndex]
852
- if (!message?.toolCalls) return
791
+ const conv = state.conversations[clientId]
792
+ const messageIndex = conv.messages.findIndex((m) => m.id === messageId)
793
+ if (messageIndex === -1) return
853
794
 
854
- const toolCallIndex = message.toolCalls.findIndex(
855
- (t) => t.id === toolCallId,
856
- )
857
- if (toolCallIndex === -1) return
795
+ const message = conv.messages[messageIndex]
796
+ if (!message?.toolCalls) return
858
797
 
859
- updateToolCall(clientId, messageIndex, toolCallIndex, {
860
- approvalRequired: true,
861
- approvalId,
862
- state: 'approval-requested',
863
- })
864
- },
865
- { withEventTarget: false },
866
- ),
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
+ }),
867
809
  )
868
810
 
869
811
  // ============= Tool Events =============
870
812
 
871
813
  cleanupFns.push(
872
- aiEventClient.on(
873
- 'tool:result-added',
874
- (e) => {
875
- const {
876
- clientId,
877
- toolCallId,
878
- toolName,
879
- output,
880
- state: resultState,
881
- timestamp,
882
- } = e.payload
883
-
884
- if (!state.conversations[clientId]) return
885
-
886
- const conv = state.conversations[clientId]
887
-
888
- // Find the message with the tool call and update it
889
- for (
890
- let messageIndex = conv.messages.length - 1;
891
- messageIndex >= 0;
892
- messageIndex--
893
- ) {
894
- const message = conv.messages[messageIndex]
895
- if (!message?.toolCalls) continue
814
+ aiEventClient.on('tool:result-added', (e) => {
815
+ const {
816
+ clientId,
817
+ toolCallId,
818
+ toolName,
819
+ output,
820
+ state: resultState,
821
+ timestamp,
822
+ } = e.payload
823
+
824
+ if (!state.conversations[clientId]) return
825
+
826
+ const conv = state.conversations[clientId]
827
+
828
+ // Find the message with the tool call and update it
829
+ for (
830
+ let messageIndex = conv.messages.length - 1;
831
+ messageIndex >= 0;
832
+ messageIndex--
833
+ ) {
834
+ const message = conv.messages[messageIndex]
835
+ if (!message?.toolCalls) continue
896
836
 
897
- const toolCallIndex = message.toolCalls.findIndex(
898
- (t: ToolCall) => t.id === toolCallId,
899
- )
900
- if (toolCallIndex >= 0) {
901
- // Update the tool call state
902
- updateToolCall(clientId, messageIndex, toolCallIndex, {
903
- result: output,
904
- state: resultState === 'output-error' ? 'error' : 'complete',
905
- })
837
+ const toolCallIndex = message.toolCalls.findIndex(
838
+ (t: ToolCall) => t.id === toolCallId,
839
+ )
840
+ if (toolCallIndex >= 0) {
841
+ // Update the tool call state
842
+ updateToolCall(clientId, messageIndex, toolCallIndex, {
843
+ result: output,
844
+ state: resultState === 'output-error' ? 'error' : 'complete',
845
+ })
906
846
 
907
- // Also add a chunk to show the tool result in the chunks view
908
- const chunk: Chunk = {
909
- id: `chunk-tool-result-${toolCallId}-${Date.now()}`,
910
- type: 'tool_result',
911
- messageId: message.id,
912
- toolCallId,
913
- toolName,
914
- result: output,
915
- timestamp,
916
- chunkCount: 1,
917
- }
918
- addChunkToMessage(clientId, chunk)
919
- return
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,
920
857
  }
858
+ addChunkToMessage(clientId, chunk)
859
+ return
921
860
  }
922
- },
923
- { withEventTarget: false },
924
- ),
861
+ }
862
+ }),
925
863
  )
926
864
 
927
865
  cleanupFns.push(
928
- aiEventClient.on(
929
- 'tool:approval-responded',
930
- (e) => {
931
- const { clientId, toolCallId, approved } = e.payload
866
+ aiEventClient.on('tool:approval-responded', (e) => {
867
+ const { clientId, toolCallId, approved } = e.payload
932
868
 
933
- if (!state.conversations[clientId]) return
869
+ if (!state.conversations[clientId]) return
934
870
 
935
- const conv = state.conversations[clientId]
871
+ const conv = state.conversations[clientId]
936
872
 
937
- for (
938
- let messageIndex = conv.messages.length - 1;
939
- messageIndex >= 0;
940
- messageIndex--
941
- ) {
942
- const message = conv.messages[messageIndex]
943
- if (!message?.toolCalls) continue
873
+ for (
874
+ let messageIndex = conv.messages.length - 1;
875
+ messageIndex >= 0;
876
+ messageIndex--
877
+ ) {
878
+ const message = conv.messages[messageIndex]
879
+ if (!message?.toolCalls) continue
944
880
 
945
- const toolCallIndex = message.toolCalls.findIndex(
946
- (t: ToolCall) => t.id === toolCallId,
947
- )
948
- if (toolCallIndex >= 0) {
949
- updateToolCall(clientId, messageIndex, toolCallIndex, {
950
- state: approved ? 'approved' : 'denied',
951
- approvalRequired: false,
952
- })
953
- return
954
- }
881
+ const toolCallIndex = message.toolCalls.findIndex(
882
+ (t: ToolCall) => t.id === toolCallId,
883
+ )
884
+ if (toolCallIndex >= 0) {
885
+ updateToolCall(clientId, messageIndex, toolCallIndex, {
886
+ state: approved ? 'approved' : 'denied',
887
+ approvalRequired: false,
888
+ })
889
+ return
955
890
  }
956
- },
957
- { withEventTarget: false },
958
- ),
891
+ }
892
+ }),
959
893
  )
960
894
 
961
895
  // ============= Stream Events =============
962
896
 
963
897
  cleanupFns.push(
964
- aiEventClient.on(
965
- 'stream:chunk:content',
966
- (e) => {
967
- const streamId = e.payload.streamId
968
- const conversationId = streamToConversation.get(streamId)
969
- if (!conversationId) return
970
-
971
- const chunk: Chunk = {
972
- id: `chunk-${Date.now()}-${Math.random()}`,
973
- type: 'content',
974
- messageId: e.payload.messageId,
975
- content: e.payload.content,
976
- delta: e.payload.delta,
977
- timestamp: e.payload.timestamp,
978
- chunkCount: 1,
979
- }
898
+ aiEventClient.on('stream:chunk:content', (e) => {
899
+ const streamId = e.payload.streamId
900
+ const conversationId = streamToConversation.get(streamId)
901
+ if (!conversationId) return
902
+
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
+ }
980
912
 
981
- const conv = state.conversations[conversationId]
982
- if (conv?.type === 'client') {
983
- addChunkToMessage(conversationId, chunk)
984
- } else {
985
- ensureMessageForChunk(
986
- conversationId,
987
- e.payload.messageId,
988
- e.payload.timestamp,
989
- )
990
- addChunk(conversationId, chunk)
991
- }
992
- },
993
- { withEventTarget: false },
994
- ),
913
+ const conv = state.conversations[conversationId]
914
+ if (conv?.type === 'client') {
915
+ addChunkToMessage(conversationId, chunk)
916
+ } else {
917
+ ensureMessageForChunk(
918
+ conversationId,
919
+ e.payload.messageId,
920
+ e.payload.timestamp,
921
+ )
922
+ addChunk(conversationId, chunk)
923
+ }
924
+ }),
995
925
  )
996
926
 
997
927
  cleanupFns.push(
998
- aiEventClient.on(
999
- 'stream:chunk:tool-call',
1000
- (e) => {
1001
- const streamId = e.payload.streamId
1002
- const conversationId = streamToConversation.get(streamId)
1003
- if (!conversationId) return
1004
-
1005
- const chunk: Chunk = {
1006
- id: `chunk-${Date.now()}-${Math.random()}`,
1007
- type: 'tool_call',
1008
- messageId: e.payload.messageId,
1009
- toolCallId: e.payload.toolCallId,
1010
- toolName: e.payload.toolName,
1011
- arguments: e.payload.arguments,
1012
- timestamp: e.payload.timestamp,
1013
- chunkCount: 1,
1014
- }
928
+ aiEventClient.on('stream:chunk:tool-call', (e) => {
929
+ const streamId = e.payload.streamId
930
+ const conversationId = streamToConversation.get(streamId)
931
+ if (!conversationId) return
932
+
933
+ const chunk: Chunk = {
934
+ id: `chunk-${Date.now()}-${Math.random()}`,
935
+ 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,
941
+ chunkCount: 1,
942
+ }
1015
943
 
1016
- const conv = state.conversations[conversationId]
1017
- if (conv?.type === 'client') {
1018
- addChunkToMessage(conversationId, chunk)
1019
- } else {
1020
- ensureMessageForChunk(
1021
- conversationId,
1022
- e.payload.messageId,
1023
- e.payload.timestamp,
1024
- )
1025
- addChunk(conversationId, chunk)
1026
- }
1027
- },
1028
- { withEventTarget: false },
1029
- ),
944
+ const conv = state.conversations[conversationId]
945
+ if (conv?.type === 'client') {
946
+ addChunkToMessage(conversationId, chunk)
947
+ } else {
948
+ ensureMessageForChunk(
949
+ conversationId,
950
+ e.payload.messageId,
951
+ e.payload.timestamp,
952
+ )
953
+ addChunk(conversationId, chunk)
954
+ }
955
+ }),
1030
956
  )
1031
957
 
1032
958
  cleanupFns.push(
1033
- aiEventClient.on(
1034
- 'stream:chunk:tool-result',
1035
- (e) => {
1036
- const streamId = e.payload.streamId
1037
- const conversationId = streamToConversation.get(streamId)
1038
- if (!conversationId) return
1039
-
1040
- const chunk: Chunk = {
1041
- id: `chunk-${Date.now()}-${Math.random()}`,
1042
- type: 'tool_result',
1043
- messageId: e.payload.messageId,
1044
- toolCallId: e.payload.toolCallId,
1045
- content: e.payload.result,
1046
- timestamp: e.payload.timestamp,
1047
- chunkCount: 1,
1048
- }
959
+ aiEventClient.on('stream:chunk:tool-result', (e) => {
960
+ const streamId = e.payload.streamId
961
+ const conversationId = streamToConversation.get(streamId)
962
+ if (!conversationId) return
963
+
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
973
 
1050
- const conv = state.conversations[conversationId]
1051
- if (conv?.type === 'client') {
1052
- addChunkToMessage(conversationId, chunk)
1053
- } else {
1054
- ensureMessageForChunk(
1055
- conversationId,
1056
- e.payload.messageId,
1057
- e.payload.timestamp,
1058
- )
1059
- addChunk(conversationId, chunk)
1060
- }
974
+ const conv = state.conversations[conversationId]
975
+ if (conv?.type === 'client') {
976
+ addChunkToMessage(conversationId, chunk)
977
+ } else {
978
+ ensureMessageForChunk(
979
+ conversationId,
980
+ e.payload.messageId,
981
+ e.payload.timestamp,
982
+ )
983
+ addChunk(conversationId, chunk)
984
+ }
1061
985
 
1062
- // Also update the toolCalls array with the result
1063
- if (conv && e.payload.toolCallId) {
1064
- for (let i = conv.messages.length - 1; i >= 0; i--) {
1065
- const message = conv.messages[i]
1066
- if (!message?.toolCalls) continue
1067
-
1068
- const toolCallIndex = message.toolCalls.findIndex(
1069
- (t) => t.id === e.payload.toolCallId,
1070
- )
1071
- if (toolCallIndex >= 0) {
1072
- updateToolCall(conversationId, i, toolCallIndex, {
1073
- result: e.payload.result,
1074
- state: 'complete',
1075
- })
1076
- break
1077
- }
986
+ // Also update the toolCalls array with the result
987
+ if (conv && e.payload.toolCallId) {
988
+ for (let i = conv.messages.length - 1; i >= 0; i--) {
989
+ const message = conv.messages[i]
990
+ if (!message?.toolCalls) continue
991
+
992
+ const toolCallIndex = message.toolCalls.findIndex(
993
+ (t) => t.id === e.payload.toolCallId,
994
+ )
995
+ if (toolCallIndex >= 0) {
996
+ updateToolCall(conversationId, i, toolCallIndex, {
997
+ result: e.payload.result,
998
+ state: 'complete',
999
+ })
1000
+ break
1078
1001
  }
1079
1002
  }
1080
- },
1081
- { withEventTarget: false },
1082
- ),
1003
+ }
1004
+ }),
1083
1005
  )
1084
1006
 
1085
1007
  cleanupFns.push(
1086
- aiEventClient.on(
1087
- 'stream:chunk:thinking',
1088
- (e) => {
1089
- const streamId = e.payload.streamId
1090
- const conversationId = streamToConversation.get(streamId)
1091
- if (!conversationId) return
1092
-
1093
- const chunk: Chunk = {
1094
- id: `chunk-${Date.now()}-${Math.random()}`,
1095
- type: 'thinking',
1096
- messageId: e.payload.messageId,
1097
- content: e.payload.content,
1098
- delta: e.payload.delta,
1099
- timestamp: e.payload.timestamp,
1100
- chunkCount: 1,
1101
- }
1008
+ aiEventClient.on('stream:chunk:thinking', (e) => {
1009
+ const streamId = e.payload.streamId
1010
+ const conversationId = streamToConversation.get(streamId)
1011
+ if (!conversationId) return
1012
+
1013
+ const chunk: Chunk = {
1014
+ id: `chunk-${Date.now()}-${Math.random()}`,
1015
+ type: 'thinking',
1016
+ messageId: e.payload.messageId,
1017
+ content: e.payload.content,
1018
+ delta: e.payload.delta,
1019
+ timestamp: e.payload.timestamp,
1020
+ chunkCount: 1,
1021
+ }
1102
1022
 
1103
- const conv = state.conversations[conversationId]
1104
- if (conv?.type === 'client') {
1105
- addChunkToMessage(conversationId, chunk)
1106
-
1107
- if (e.payload.messageId) {
1108
- const messageIndex = conv.messages.findIndex(
1109
- (msg) => msg.id === e.payload.messageId,
1110
- )
1111
- if (messageIndex !== -1) {
1112
- updateMessage(conversationId, messageIndex, {
1113
- thinkingContent: e.payload.content,
1114
- })
1115
- }
1116
- }
1117
- } else {
1118
- ensureMessageForChunk(
1119
- conversationId,
1120
- e.payload.messageId,
1121
- e.payload.timestamp,
1023
+ const conv = state.conversations[conversationId]
1024
+ if (conv?.type === 'client') {
1025
+ addChunkToMessage(conversationId, chunk)
1026
+
1027
+ if (e.payload.messageId) {
1028
+ const messageIndex = conv.messages.findIndex(
1029
+ (msg) => msg.id === e.payload.messageId,
1122
1030
  )
1123
- addChunk(conversationId, chunk)
1031
+ if (messageIndex !== -1) {
1032
+ updateMessage(conversationId, messageIndex, {
1033
+ thinkingContent: e.payload.content,
1034
+ })
1035
+ }
1124
1036
  }
1125
- },
1126
- { withEventTarget: false },
1127
- ),
1037
+ } else {
1038
+ ensureMessageForChunk(
1039
+ conversationId,
1040
+ e.payload.messageId,
1041
+ e.payload.timestamp,
1042
+ )
1043
+ addChunk(conversationId, chunk)
1044
+ }
1045
+ }),
1128
1046
  )
1129
1047
 
1130
1048
  cleanupFns.push(
1131
- aiEventClient.on(
1132
- 'stream:chunk:done',
1133
- (e) => {
1134
- const streamId = e.payload.streamId
1135
- const conversationId = streamToConversation.get(streamId)
1136
- if (!conversationId) return
1137
-
1138
- const chunk: Chunk = {
1139
- id: `chunk-${Date.now()}-${Math.random()}`,
1140
- type: 'done',
1141
- messageId: e.payload.messageId,
1142
- finishReason: e.payload.finishReason || undefined,
1143
- timestamp: e.payload.timestamp,
1144
- chunkCount: 1,
1145
- }
1049
+ aiEventClient.on('stream:chunk:done', (e) => {
1050
+ const streamId = e.payload.streamId
1051
+ const conversationId = streamToConversation.get(streamId)
1052
+ if (!conversationId) return
1053
+
1054
+ const chunk: Chunk = {
1055
+ id: `chunk-${Date.now()}-${Math.random()}`,
1056
+ type: 'done',
1057
+ messageId: e.payload.messageId,
1058
+ finishReason: e.payload.finishReason || undefined,
1059
+ timestamp: e.payload.timestamp,
1060
+ chunkCount: 1,
1061
+ }
1146
1062
 
1147
- if (e.payload.usage) {
1148
- updateConversation(conversationId, { usage: e.payload.usage })
1149
- updateMessageUsage(
1150
- conversationId,
1151
- e.payload.messageId,
1152
- e.payload.usage,
1153
- )
1154
- }
1063
+ if (e.payload.usage) {
1064
+ updateConversation(conversationId, { usage: e.payload.usage })
1065
+ updateMessageUsage(
1066
+ conversationId,
1067
+ e.payload.messageId,
1068
+ e.payload.usage,
1069
+ )
1070
+ }
1155
1071
 
1156
- const conv = state.conversations[conversationId]
1157
- if (conv?.type === 'client') {
1158
- addChunkToMessage(conversationId, chunk)
1159
- } else {
1160
- ensureMessageForChunk(
1161
- conversationId,
1162
- e.payload.messageId,
1163
- e.payload.timestamp,
1164
- )
1165
- addChunk(conversationId, chunk)
1166
- }
1072
+ const conv = state.conversations[conversationId]
1073
+ if (conv?.type === 'client') {
1074
+ addChunkToMessage(conversationId, chunk)
1075
+ } else {
1076
+ ensureMessageForChunk(
1077
+ conversationId,
1078
+ e.payload.messageId,
1079
+ e.payload.timestamp,
1080
+ )
1081
+ addChunk(conversationId, chunk)
1082
+ }
1167
1083
 
1168
- updateConversation(conversationId, {
1169
- status: 'completed',
1170
- completedAt: e.payload.timestamp,
1171
- })
1172
- },
1173
- { withEventTarget: false },
1174
- ),
1084
+ updateConversation(conversationId, {
1085
+ status: 'completed',
1086
+ completedAt: e.payload.timestamp,
1087
+ })
1088
+ }),
1175
1089
  )
1176
1090
 
1177
1091
  cleanupFns.push(
1178
- aiEventClient.on(
1179
- 'stream:chunk:error',
1180
- (e) => {
1181
- const streamId = e.payload.streamId
1182
- const conversationId = streamToConversation.get(streamId)
1183
- if (!conversationId) return
1184
-
1185
- const chunk: Chunk = {
1186
- id: `chunk-${Date.now()}-${Math.random()}`,
1187
- type: 'error',
1188
- messageId: e.payload.messageId,
1189
- error: e.payload.error,
1190
- timestamp: e.payload.timestamp,
1191
- chunkCount: 1,
1192
- }
1092
+ aiEventClient.on('stream:chunk:error', (e) => {
1093
+ const streamId = e.payload.streamId
1094
+ const conversationId = streamToConversation.get(streamId)
1095
+ if (!conversationId) return
1096
+
1097
+ const chunk: Chunk = {
1098
+ id: `chunk-${Date.now()}-${Math.random()}`,
1099
+ type: 'error',
1100
+ messageId: e.payload.messageId,
1101
+ error: e.payload.error,
1102
+ timestamp: e.payload.timestamp,
1103
+ chunkCount: 1,
1104
+ }
1193
1105
 
1194
- const conv = state.conversations[conversationId]
1195
- if (conv?.type === 'client') {
1196
- addChunkToMessage(conversationId, chunk)
1197
- } else {
1198
- ensureMessageForChunk(
1199
- conversationId,
1200
- e.payload.messageId,
1201
- e.payload.timestamp,
1202
- )
1203
- addChunk(conversationId, chunk)
1204
- }
1106
+ const conv = state.conversations[conversationId]
1107
+ if (conv?.type === 'client') {
1108
+ addChunkToMessage(conversationId, chunk)
1109
+ } else {
1110
+ ensureMessageForChunk(
1111
+ conversationId,
1112
+ e.payload.messageId,
1113
+ e.payload.timestamp,
1114
+ )
1115
+ addChunk(conversationId, chunk)
1116
+ }
1205
1117
 
1206
- updateConversation(conversationId, {
1207
- status: 'error',
1208
- completedAt: e.payload.timestamp,
1209
- })
1210
- },
1211
- { withEventTarget: false },
1212
- ),
1118
+ updateConversation(conversationId, {
1119
+ status: 'error',
1120
+ completedAt: e.payload.timestamp,
1121
+ })
1122
+ }),
1213
1123
  )
1214
1124
 
1215
1125
  cleanupFns.push(
1216
- aiEventClient.on(
1217
- 'stream:ended',
1218
- (e) => {
1219
- const streamId = e.payload.streamId
1220
- const conversationId = streamToConversation.get(streamId)
1221
- if (!conversationId) return
1222
-
1223
- updateConversation(conversationId, {
1224
- status: 'completed',
1225
- completedAt: e.payload.timestamp,
1226
- })
1227
- },
1228
- { withEventTarget: false },
1229
- ),
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
+ }),
1230
1136
  )
1231
1137
 
1232
1138
  cleanupFns.push(
1233
- aiEventClient.on(
1234
- 'stream:approval-requested',
1235
- (e) => {
1236
- const {
1237
- streamId,
1238
- messageId,
1239
- toolCallId,
1240
- toolName,
1241
- input,
1242
- approvalId,
1243
- timestamp,
1244
- } = e.payload
1245
-
1246
- const conversationId = streamToConversation.get(streamId)
1247
- if (!conversationId) return
1248
-
1249
- const conv = state.conversations[conversationId]
1250
- if (!conv) return
1251
-
1252
- const chunk: Chunk = {
1253
- id: `chunk-${Date.now()}-${Math.random()}`,
1254
- type: 'approval',
1255
- messageId: messageId,
1256
- toolCallId,
1257
- toolName,
1258
- approvalId,
1259
- input,
1260
- timestamp,
1261
- chunkCount: 1,
1262
- }
1139
+ aiEventClient.on('stream:approval-requested', (e) => {
1140
+ const {
1141
+ streamId,
1142
+ messageId,
1143
+ toolCallId,
1144
+ toolName,
1145
+ input,
1146
+ approvalId,
1147
+ timestamp,
1148
+ } = e.payload
1149
+
1150
+ const conversationId = streamToConversation.get(streamId)
1151
+ if (!conversationId) return
1263
1152
 
1264
- if (conv.type === 'client') {
1265
- addChunkToMessage(conversationId, chunk)
1266
- } else {
1267
- addChunk(conversationId, chunk)
1268
- }
1153
+ const conv = state.conversations[conversationId]
1154
+ if (!conv) return
1155
+
1156
+ const chunk: Chunk = {
1157
+ id: `chunk-${Date.now()}-${Math.random()}`,
1158
+ type: 'approval',
1159
+ messageId: messageId,
1160
+ toolCallId,
1161
+ toolName,
1162
+ approvalId,
1163
+ input,
1164
+ timestamp,
1165
+ chunkCount: 1,
1166
+ }
1269
1167
 
1270
- for (let i = conv.messages.length - 1; i >= 0; i--) {
1271
- const message = conv.messages[i]
1272
- if (!message) continue
1273
-
1274
- if (message.role === 'assistant' && message.toolCalls) {
1275
- const toolCallIndex = message.toolCalls.findIndex(
1276
- (t: ToolCall) => t.id === toolCallId,
1277
- )
1278
- if (toolCallIndex >= 0) {
1279
- updateToolCall(conversationId, i, toolCallIndex, {
1280
- approvalRequired: true,
1281
- approvalId,
1282
- state: 'approval-requested',
1283
- })
1284
- return
1285
- }
1168
+ if (conv.type === 'client') {
1169
+ addChunkToMessage(conversationId, chunk)
1170
+ } else {
1171
+ addChunk(conversationId, chunk)
1172
+ }
1173
+
1174
+ for (let i = conv.messages.length - 1; i >= 0; i--) {
1175
+ const message = conv.messages[i]
1176
+ if (!message) continue
1177
+
1178
+ if (message.role === 'assistant' && message.toolCalls) {
1179
+ const toolCallIndex = message.toolCalls.findIndex(
1180
+ (t: ToolCall) => t.id === toolCallId,
1181
+ )
1182
+ if (toolCallIndex >= 0) {
1183
+ updateToolCall(conversationId, i, toolCallIndex, {
1184
+ approvalRequired: true,
1185
+ approvalId,
1186
+ state: 'approval-requested',
1187
+ })
1188
+ return
1286
1189
  }
1287
1190
  }
1288
- },
1289
- { withEventTarget: false },
1290
- ),
1191
+ }
1192
+ }),
1291
1193
  )
1292
1194
 
1293
1195
  // ============= Processor Events =============
1294
1196
 
1295
1197
  cleanupFns.push(
1296
- aiEventClient.on(
1297
- 'processor:text-updated',
1298
- (e) => {
1299
- const streamId = e.payload.streamId
1198
+ aiEventClient.on('processor:text-updated', (e) => {
1199
+ const streamId = e.payload.streamId
1300
1200
 
1301
- let conversationId = streamToConversation.get(streamId)
1201
+ let conversationId = streamToConversation.get(streamId)
1302
1202
 
1303
- if (!conversationId) {
1304
- const activeClients = Object.values(state.conversations)
1305
- .filter((c) => c.type === 'client' && c.status === 'active')
1306
- .sort((a, b) => b.startedAt - a.startedAt)
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)
1307
1207
 
1308
- if (activeClients.length > 0 && activeClients[0]) {
1309
- conversationId = activeClients[0].id
1310
- streamToConversation.set(streamId, conversationId)
1311
- }
1208
+ if (activeClients.length > 0 && activeClients[0]) {
1209
+ conversationId = activeClients[0].id
1210
+ streamToConversation.set(streamId, conversationId)
1312
1211
  }
1212
+ }
1313
1213
 
1314
- if (!conversationId) return
1214
+ if (!conversationId) return
1315
1215
 
1316
- const conv = state.conversations[conversationId]
1317
- if (!conv) return
1216
+ const conv = state.conversations[conversationId]
1217
+ if (!conv) return
1318
1218
 
1319
- // Only update existing assistant messages, don't create new ones
1320
- // (client:message-appended is responsible for creating messages)
1321
- const lastMessage = conv.messages[conv.messages.length - 1]
1322
- if (lastMessage && lastMessage.role === 'assistant') {
1323
- updateMessage(conversationId, conv.messages.length - 1, {
1324
- content: e.payload.content,
1325
- })
1326
- }
1327
- },
1328
- { withEventTarget: false },
1329
- ),
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
+ }),
1330
1228
  )
1331
1229
 
1332
1230
  cleanupFns.push(
1333
- aiEventClient.on(
1334
- 'processor:tool-call-state-changed',
1335
- (e) => {
1336
- const streamId = e.payload.streamId
1337
- const conversationId = streamToConversation.get(streamId)
1231
+ aiEventClient.on('processor:tool-call-state-changed', (e) => {
1232
+ const streamId = e.payload.streamId
1233
+ const conversationId = streamToConversation.get(streamId)
1338
1234
 
1339
- if (!conversationId || !state.conversations[conversationId]) return
1235
+ if (!conversationId || !state.conversations[conversationId]) return
1340
1236
 
1341
- const conv = state.conversations[conversationId]
1342
- const lastMessage = conv.messages[conv.messages.length - 1]
1237
+ const conv = state.conversations[conversationId]
1238
+ const lastMessage = conv.messages[conv.messages.length - 1]
1343
1239
 
1344
- if (lastMessage && lastMessage.role === 'assistant') {
1345
- const toolCalls = lastMessage.toolCalls || []
1346
- const existingToolIndex = toolCalls.findIndex(
1347
- (t) => t.id === e.payload.toolCallId,
1348
- )
1240
+ if (lastMessage && lastMessage.role === 'assistant') {
1241
+ const toolCalls = lastMessage.toolCalls || []
1242
+ const existingToolIndex = toolCalls.findIndex(
1243
+ (t) => t.id === e.payload.toolCallId,
1244
+ )
1349
1245
 
1350
- const toolCall: ToolCall = {
1351
- id: e.payload.toolCallId,
1352
- name: e.payload.toolName,
1353
- arguments: JSON.stringify(e.payload.arguments, null, 2),
1354
- state: e.payload.state,
1355
- }
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
+ }
1356
1252
 
1357
- if (existingToolIndex >= 0) {
1358
- updateToolCall(
1359
- conversationId,
1360
- conv.messages.length - 1,
1361
- existingToolIndex,
1362
- toolCall,
1363
- )
1364
- } else {
1365
- setToolCalls(conversationId, conv.messages.length - 1, [
1366
- ...toolCalls,
1367
- toolCall,
1368
- ])
1369
- }
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
+ ])
1370
1265
  }
1371
- },
1372
- { withEventTarget: false },
1373
- ),
1266
+ }
1267
+ }),
1374
1268
  )
1375
1269
 
1376
1270
  cleanupFns.push(
1377
- aiEventClient.on(
1378
- 'processor:tool-result-state-changed',
1379
- (e) => {
1380
- const streamId = e.payload.streamId
1381
- const conversationId = streamToConversation.get(streamId)
1271
+ aiEventClient.on('processor:tool-result-state-changed', (e) => {
1272
+ const streamId = e.payload.streamId
1273
+ const conversationId = streamToConversation.get(streamId)
1382
1274
 
1383
- if (!conversationId || !state.conversations[conversationId]) return
1275
+ if (!conversationId || !state.conversations[conversationId]) return
1384
1276
 
1385
- const conv = state.conversations[conversationId]
1277
+ const conv = state.conversations[conversationId]
1386
1278
 
1387
- for (let i = conv.messages.length - 1; i >= 0; i--) {
1388
- const message = conv.messages[i]
1389
- if (!message?.toolCalls) continue
1279
+ for (let i = conv.messages.length - 1; i >= 0; i--) {
1280
+ const message = conv.messages[i]
1281
+ if (!message?.toolCalls) continue
1390
1282
 
1391
- const toolCallIndex = message.toolCalls.findIndex(
1392
- (t) => t.id === e.payload.toolCallId,
1393
- )
1394
- if (toolCallIndex >= 0) {
1395
- updateToolCall(conversationId, i, toolCallIndex, {
1396
- result: e.payload.content,
1397
- state: e.payload.error ? 'error' : e.payload.state,
1398
- })
1399
- return
1400
- }
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
1401
1292
  }
1402
- },
1403
- { withEventTarget: false },
1404
- ),
1293
+ }
1294
+ }),
1405
1295
  )
1406
1296
 
1407
1297
  // ============= Chat Events (for usage tracking) =============
1408
1298
 
1409
1299
  cleanupFns.push(
1410
- aiEventClient.on(
1411
- 'chat:started',
1412
- (e) => {
1413
- const streamId = e.payload.streamId
1414
- const model = e.payload.model
1415
- const provider = e.payload.provider
1416
- const clientId = e.payload.clientId
1417
-
1418
- if (clientId && state.conversations[clientId]) {
1419
- streamToConversation.set(streamId, clientId)
1420
- updateConversation(clientId, { status: 'active', ...e.payload })
1421
- return
1422
- }
1300
+ aiEventClient.on('text:started', (e) => {
1301
+ const streamId = e.payload.streamId
1302
+ const model = e.payload.model
1303
+ const provider = e.payload.provider
1304
+ const clientId = e.payload.clientId
1305
+
1306
+ if (clientId && state.conversations[clientId]) {
1307
+ streamToConversation.set(streamId, clientId)
1308
+ updateConversation(clientId, { status: 'active', ...e.payload })
1309
+ return
1310
+ }
1423
1311
 
1424
- const activeClient = Object.values(state.conversations).find(
1425
- (c) => c.type === 'client' && c.status === 'active' && !c.model,
1312
+ const activeClient = Object.values(state.conversations).find(
1313
+ (c) => c.type === 'client' && c.status === 'active' && !c.model,
1314
+ )
1315
+
1316
+ if (activeClient) {
1317
+ streamToConversation.set(streamId, activeClient.id)
1318
+ updateConversation(activeClient.id, { ...e.payload })
1319
+ } else {
1320
+ const existingServerConv = Object.values(state.conversations).find(
1321
+ (c) => c.type === 'server' && c.model === model,
1426
1322
  )
1427
1323
 
1428
- if (activeClient) {
1429
- streamToConversation.set(streamId, activeClient.id)
1430
- updateConversation(activeClient.id, { ...e.payload })
1324
+ if (existingServerConv) {
1325
+ streamToConversation.set(streamId, existingServerConv.id)
1326
+ updateConversation(existingServerConv.id, {
1327
+ status: 'active',
1328
+ ...e.payload,
1329
+ })
1431
1330
  } else {
1432
- const existingServerConv = Object.values(state.conversations).find(
1433
- (c) => c.type === 'server' && c.model === model,
1434
- )
1435
-
1436
- if (existingServerConv) {
1437
- streamToConversation.set(streamId, existingServerConv.id)
1438
- updateConversation(existingServerConv.id, {
1439
- status: 'active',
1440
- ...e.payload,
1441
- })
1442
- } else {
1443
- const serverId = `server-${model}`
1444
- getOrCreateConversation(serverId, 'server', `${model} Server`)
1445
- streamToConversation.set(streamId, serverId)
1446
- updateConversation(serverId, { ...e.payload })
1447
- }
1331
+ const serverId = `server-${model}`
1332
+ getOrCreateConversation(serverId, 'server', `${model} Server`)
1333
+ streamToConversation.set(streamId, serverId)
1334
+ updateConversation(serverId, { ...e.payload })
1448
1335
  }
1449
- },
1450
- { withEventTarget: false },
1451
- ),
1336
+ }
1337
+ }),
1452
1338
  )
1453
1339
 
1454
1340
  cleanupFns.push(
1455
- aiEventClient.on(
1456
- 'chat:completed',
1457
- (e) => {
1458
- const { requestId, usage } = e.payload
1459
-
1460
- const conversationId = requestToConversation.get(requestId)
1461
- if (conversationId && state.conversations[conversationId]) {
1462
- const updates: Partial<Conversation> = {
1463
- status: 'completed',
1464
- completedAt: e.payload.timestamp,
1465
- }
1466
- if (usage) {
1467
- updates.usage = usage
1468
- }
1469
- updateConversation(conversationId, updates)
1470
- if (usage) {
1471
- updateMessageUsage(conversationId, e.payload.messageId, usage)
1472
- }
1473
- }
1474
- },
1475
- { withEventTarget: false },
1476
- ),
1477
- )
1341
+ aiEventClient.on('text:completed', (e) => {
1342
+ const { requestId, usage } = e.payload
1478
1343
 
1479
- cleanupFns.push(
1480
- aiEventClient.on(
1481
- 'chat:iteration',
1482
- (e) => {
1483
- const { requestId, iterationNumber } = e.payload
1484
-
1485
- const conversationId = requestToConversation.get(requestId)
1486
- if (conversationId && state.conversations[conversationId]) {
1487
- updateConversation(conversationId, {
1488
- iterationCount: iterationNumber,
1489
- })
1344
+ const conversationId = requestToConversation.get(requestId)
1345
+ if (conversationId && state.conversations[conversationId]) {
1346
+ const updates: Partial<Conversation> = {
1347
+ status: 'completed',
1348
+ completedAt: e.payload.timestamp,
1490
1349
  }
1491
- },
1492
- { withEventTarget: false },
1493
- ),
1494
- )
1495
-
1496
- cleanupFns.push(
1497
- aiEventClient.on(
1498
- 'usage:tokens',
1499
- (e) => {
1500
- const { requestId, usage, messageId } = e.payload
1501
-
1502
- const conversationId = requestToConversation.get(requestId)
1503
- if (conversationId && state.conversations[conversationId]) {
1504
- updateConversation(conversationId, { usage })
1505
- updateMessageUsage(conversationId, messageId, usage)
1350
+ if (usage) {
1351
+ updates.usage = usage
1506
1352
  }
1507
- },
1508
- { withEventTarget: false },
1509
- ),
1353
+ updateConversation(conversationId, updates)
1354
+ if (usage) {
1355
+ updateMessageUsage(conversationId, e.payload.messageId, usage)
1356
+ }
1357
+ }
1358
+ }),
1510
1359
  )
1511
1360
 
1512
- // ============= Tool Call Completed (with duration) =============
1513
-
1514
1361
  cleanupFns.push(
1515
- aiEventClient.on(
1516
- 'tool:call-completed',
1517
- (e) => {
1518
- const {
1519
- streamId,
1520
- toolCallId,
1521
- toolName,
1522
- result,
1523
- duration,
1524
- messageId,
1525
- timestamp,
1526
- } = e.payload
1527
-
1528
- const conversationId = streamToConversation.get(streamId)
1529
- if (!conversationId || !state.conversations[conversationId]) return
1530
-
1531
- const conv = state.conversations[conversationId]
1532
-
1533
- // Add a tool_result chunk to show the result in the chunks view
1534
- const chunk: Chunk = {
1535
- id: `chunk-tool-result-${toolCallId}-${Date.now()}`,
1536
- type: 'tool_result',
1537
- messageId: messageId,
1538
- toolCallId,
1539
- toolName,
1540
- result,
1541
- duration,
1542
- timestamp,
1543
- chunkCount: 1,
1544
- }
1362
+ aiEventClient.on('text:iteration', (e) => {
1363
+ const { requestId, iterationNumber } = e.payload
1545
1364
 
1546
- // Add chunk to message if it's a client conversation, otherwise to conversation
1547
- if (conv.type === 'client' && messageId) {
1548
- const messageIndex = conv.messages.findIndex(
1549
- (m) => m.id === messageId,
1550
- )
1551
- if (messageIndex !== -1) {
1552
- queueMessageChunk(conversationId, messageIndex, chunk)
1553
- } else {
1554
- // If message not found, add to last assistant message
1555
- for (let i = conv.messages.length - 1; i >= 0; i--) {
1556
- if (conv.messages[i]?.role === 'assistant') {
1557
- queueMessageChunk(conversationId, i, chunk)
1558
- break
1559
- }
1560
- }
1561
- }
1562
- } else {
1563
- addChunk(conversationId, chunk)
1564
- }
1365
+ const conversationId = requestToConversation.get(requestId)
1366
+ if (conversationId && state.conversations[conversationId]) {
1367
+ updateConversation(conversationId, {
1368
+ iterationCount: iterationNumber,
1369
+ })
1370
+ }
1371
+ }),
1372
+ )
1565
1373
 
1566
- // Update the tool call with duration
1567
- for (let i = conv.messages.length - 1; i >= 0; i--) {
1568
- const message = conv.messages[i]
1569
- if (!message?.toolCalls) continue
1374
+ cleanupFns.push(
1375
+ aiEventClient.on('usage:tokens', (e) => {
1376
+ const { requestId, usage, messageId } = e.payload
1570
1377
 
1571
- const toolCallIndex = message.toolCalls.findIndex(
1572
- (t) => t.id === toolCallId,
1573
- )
1574
- if (toolCallIndex >= 0) {
1575
- updateToolCall(conversationId, i, toolCallIndex, {
1576
- duration,
1577
- result,
1578
- })
1579
- return
1580
- }
1581
- }
1582
- },
1583
- { withEventTarget: false },
1584
- ),
1378
+ const conversationId = requestToConversation.get(requestId)
1379
+ if (conversationId && state.conversations[conversationId]) {
1380
+ updateConversation(conversationId, { usage })
1381
+ updateMessageUsage(conversationId, messageId, usage)
1382
+ }
1383
+ }),
1585
1384
  )
1586
1385
 
1587
- // ============= Embedding Events =============
1386
+ // ============= Tool Call Completed (with duration) =============
1588
1387
 
1589
1388
  cleanupFns.push(
1590
- aiEventClient.on(
1591
- 'embedding:started',
1592
- (e) => {
1593
- const { requestId, model, inputCount, timestamp, clientId } =
1594
- e.payload
1595
-
1596
- // Try to find an active conversation to attach to, or create a new one
1597
- let conversationId = clientId
1598
- if (!conversationId || !state.conversations[conversationId]) {
1599
- // Find most recent active client conversation
1600
- const activeClients = Object.values(state.conversations)
1601
- .filter((c) => c.type === 'client' && c.status === 'active')
1602
- .sort((a, b) => b.startedAt - a.startedAt)
1603
-
1604
- if (activeClients.length > 0 && activeClients[0]) {
1605
- conversationId = activeClients[0].id
1606
- } else {
1607
- // Create a new conversation for embeddings
1608
- conversationId = `embedding-${requestId}`
1609
- getOrCreateConversation(
1610
- conversationId,
1611
- 'server',
1612
- `Embedding (${model})`,
1613
- )
1614
- updateConversation(conversationId, { model })
1615
- }
1616
- }
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
1617
1402
 
1618
- requestToConversation.set(requestId, conversationId)
1403
+ const conv = state.conversations[conversationId]
1619
1404
 
1620
- const embeddingOp: EmbeddingOperation = {
1621
- id: requestId,
1622
- model,
1623
- inputCount,
1624
- duration: 0,
1625
- timestamp,
1626
- status: 'started',
1627
- }
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
+ }
1628
1417
 
1629
- const conv = state.conversations[conversationId]
1630
- if (conv) {
1631
- const embeddings = conv.embeddings || []
1632
- setState('conversations', conversationId, 'embeddings', [
1633
- ...embeddings,
1634
- embeddingOp,
1635
- ])
1636
- setState('conversations', conversationId, 'hasEmbedding', true)
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
+ }
1637
1433
  }
1638
- },
1639
- { withEventTarget: false },
1640
- ),
1641
- )
1642
-
1643
- cleanupFns.push(
1644
- aiEventClient.on(
1645
- 'embedding:completed',
1646
- (e) => {
1647
- const { requestId, duration } = e.payload
1648
-
1649
- const conversationId = requestToConversation.get(requestId)
1650
- if (!conversationId || !state.conversations[conversationId]) return
1434
+ } else {
1435
+ addChunk(conversationId, chunk)
1436
+ }
1651
1437
 
1652
- const conv = state.conversations[conversationId]
1653
- if (!conv.embeddings) return
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
1654
1442
 
1655
- const embeddingIndex = conv.embeddings.findIndex(
1656
- (op) => op.id === requestId,
1443
+ const toolCallIndex = message.toolCalls.findIndex(
1444
+ (t) => t.id === toolCallId,
1657
1445
  )
1658
- if (embeddingIndex >= 0) {
1659
- setState(
1660
- 'conversations',
1661
- conversationId,
1662
- 'embeddings',
1663
- embeddingIndex,
1664
- produce((op: EmbeddingOperation) => {
1665
- op.duration = duration
1666
- op.status = 'completed'
1667
- }),
1668
- )
1446
+ if (toolCallIndex >= 0) {
1447
+ updateToolCall(conversationId, i, toolCallIndex, {
1448
+ duration,
1449
+ result,
1450
+ })
1451
+ return
1669
1452
  }
1670
- },
1671
- { withEventTarget: false },
1672
- ),
1453
+ }
1454
+ }),
1673
1455
  )
1674
1456
 
1675
1457
  // ============= Summarize Events =============
1676
1458
 
1677
1459
  cleanupFns.push(
1678
- aiEventClient.on(
1679
- 'summarize:started',
1680
- (e) => {
1681
- const { requestId, model, inputLength, timestamp, clientId } =
1682
- e.payload
1683
-
1684
- // Try to find an active conversation to attach to, or create a new one
1685
- let conversationId = clientId
1686
- if (!conversationId || !state.conversations[conversationId]) {
1687
- // Find most recent active client conversation
1688
- const activeClients = Object.values(state.conversations)
1689
- .filter((c) => c.type === 'client' && c.status === 'active')
1690
- .sort((a, b) => b.startedAt - a.startedAt)
1691
-
1692
- if (activeClients.length > 0 && activeClients[0]) {
1693
- conversationId = activeClients[0].id
1694
- } else {
1695
- // Create a new conversation for summaries
1696
- conversationId = `summarize-${requestId}`
1697
- getOrCreateConversation(
1698
- conversationId,
1699
- 'server',
1700
- `Summarize (${model})`,
1701
- )
1702
- updateConversation(conversationId, { model })
1703
- }
1460
+ aiEventClient.on('summarize:started', (e) => {
1461
+ const { requestId, model, inputLength, timestamp, clientId } = e.payload
1462
+
1463
+ // Try to find an active conversation to attach to, or create a new one
1464
+ let conversationId = clientId
1465
+ if (!conversationId || !state.conversations[conversationId]) {
1466
+ // Find most recent active client conversation
1467
+ const activeClients = Object.values(state.conversations)
1468
+ .filter((c) => c.type === 'client' && c.status === 'active')
1469
+ .sort((a, b) => b.startedAt - a.startedAt)
1470
+
1471
+ if (activeClients.length > 0 && activeClients[0]) {
1472
+ conversationId = activeClients[0].id
1473
+ } else {
1474
+ // Create a new conversation for summaries
1475
+ conversationId = `summarize-${requestId}`
1476
+ getOrCreateConversation(
1477
+ conversationId,
1478
+ 'server',
1479
+ `Summarize (${model})`,
1480
+ )
1481
+ updateConversation(conversationId, { model })
1704
1482
  }
1483
+ }
1705
1484
 
1706
- requestToConversation.set(requestId, conversationId)
1485
+ requestToConversation.set(requestId, conversationId)
1707
1486
 
1708
- const summarizeOp: SummarizeOperation = {
1709
- id: requestId,
1710
- model,
1711
- inputLength,
1712
- timestamp,
1713
- status: 'started',
1714
- }
1487
+ const summarizeOp: SummarizeOperation = {
1488
+ id: requestId,
1489
+ model,
1490
+ inputLength,
1491
+ timestamp,
1492
+ status: 'started',
1493
+ }
1715
1494
 
1716
- const conv = state.conversations[conversationId]
1717
- if (conv) {
1718
- const summaries = conv.summaries || []
1719
- setState('conversations', conversationId, 'summaries', [
1720
- ...summaries,
1721
- summarizeOp,
1722
- ])
1723
- setState('conversations', conversationId, 'hasSummarize', true)
1724
- }
1725
- },
1726
- { withEventTarget: false },
1727
- ),
1495
+ const conv = state.conversations[conversationId]
1496
+ if (conv) {
1497
+ const summaries = conv.summaries || []
1498
+ setState('conversations', conversationId, 'summaries', [
1499
+ ...summaries,
1500
+ summarizeOp,
1501
+ ])
1502
+ setState('conversations', conversationId, 'hasSummarize', true)
1503
+ }
1504
+ }),
1728
1505
  )
1729
1506
 
1730
1507
  cleanupFns.push(
1731
- aiEventClient.on(
1732
- 'summarize:completed',
1733
- (e) => {
1734
- const { requestId, outputLength, duration } = e.payload
1508
+ aiEventClient.on('summarize:completed', (e) => {
1509
+ const { requestId, outputLength, duration } = e.payload
1735
1510
 
1736
- const conversationId = requestToConversation.get(requestId)
1737
- if (!conversationId || !state.conversations[conversationId]) return
1511
+ const conversationId = requestToConversation.get(requestId)
1512
+ if (!conversationId || !state.conversations[conversationId]) return
1738
1513
 
1739
- const conv = state.conversations[conversationId]
1740
- if (!conv.summaries) return
1514
+ const conv = state.conversations[conversationId]
1515
+ if (!conv.summaries) return
1741
1516
 
1742
- const summaryIndex = conv.summaries.findIndex(
1743
- (op) => op.id === requestId,
1517
+ const summaryIndex = conv.summaries.findIndex(
1518
+ (op) => op.id === requestId,
1519
+ )
1520
+ if (summaryIndex >= 0) {
1521
+ setState(
1522
+ 'conversations',
1523
+ conversationId,
1524
+ 'summaries',
1525
+ summaryIndex,
1526
+ produce((op: SummarizeOperation) => {
1527
+ op.duration = duration
1528
+ op.outputLength = outputLength
1529
+ op.status = 'completed'
1530
+ }),
1744
1531
  )
1745
- if (summaryIndex >= 0) {
1746
- setState(
1747
- 'conversations',
1748
- conversationId,
1749
- 'summaries',
1750
- summaryIndex,
1751
- produce((op: SummarizeOperation) => {
1752
- op.duration = duration
1753
- op.outputLength = outputLength
1754
- op.status = 'completed'
1755
- }),
1756
- )
1757
- }
1758
- },
1759
- { withEventTarget: false },
1760
- ),
1532
+ }
1533
+ }),
1761
1534
  )
1762
1535
 
1763
1536
  // Cleanup all listeners on unmount