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