@yushaw/sanqian-chat 0.2.36 → 0.2.39

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.
@@ -677,13 +677,48 @@ interface UseChatOptions {
677
677
  adapter: ChatAdapter;
678
678
  conversationId?: string;
679
679
  onError?: (error: Error) => void;
680
- onConversationChange?: (conversationId: string, title?: string) => void;
680
+ onConversationChange?: (conversationId: string, title?: string, meta?: ConversationChangeMeta) => void;
681
681
  }
682
682
  /** Options for sendMessage */
683
683
  interface SendMessageOptions {
684
684
  /** Attached resources to include with the message */
685
685
  attachedResources?: AttachedResource[];
686
686
  }
687
+ interface ConversationChangeMeta {
688
+ /**
689
+ * Source of this conversation change event:
690
+ * - "active": current foreground stream completed and updated UI state
691
+ * - "background": a detached stream completed in background
692
+ */
693
+ source: 'active' | 'background';
694
+ /** Unique stream token within this hook instance */
695
+ streamToken?: string;
696
+ /** Whether this change comes from a detached/background stream */
697
+ detached?: boolean;
698
+ /** Optional user-provided context captured when detaching a stream */
699
+ detachContext?: unknown;
700
+ }
701
+ interface ConversationSwitchOptions {
702
+ /**
703
+ * Whether to cancel the currently active stream before switching.
704
+ * - true (default): existing behavior, stream is cancelled.
705
+ * - false: detach current stream and allow it to finish in background.
706
+ */
707
+ cancelActiveStream?: boolean;
708
+ /**
709
+ * Optional metadata persisted on detached stream context.
710
+ * Returned via onConversationChange meta when detached stream completes.
711
+ */
712
+ detachContext?: unknown;
713
+ }
714
+ interface UseChatCapabilities {
715
+ conversationSwitch: {
716
+ /** Supports `cancelActiveStream` option on loadConversation/newConversation */
717
+ supportsCancelActiveStream: true;
718
+ /** Supports `detachContext` propagation for detached/background streams */
719
+ supportsDetachContext: true;
720
+ };
721
+ }
687
722
  interface UseChatReturn {
688
723
  messages: ChatMessage[];
689
724
  isLoading: boolean;
@@ -700,12 +735,14 @@ interface UseChatReturn {
700
735
  approveHitl: (remember?: boolean) => void;
701
736
  rejectHitl: (remember?: boolean) => void;
702
737
  submitHitlInput: (response: HitlResponse) => void;
703
- loadConversation: (id: string) => Promise<void>;
704
- newConversation: () => void;
738
+ loadConversation: (id: string, options?: ConversationSwitchOptions) => Promise<void>;
739
+ newConversation: (options?: ConversationSwitchOptions) => void;
705
740
  /** Session resources pushed by apps (global, persists until app disconnects) */
706
741
  sessionResources: StoredSessionResource[];
707
742
  /** Remove a session resource (user action from UI) */
708
743
  removeSessionResource: (fullId: string) => void;
744
+ /** Feature flags for safe capability-based integrations */
745
+ capabilities?: UseChatCapabilities;
709
746
  }
710
747
  declare function useChat(options: UseChatOptions): UseChatReturn;
711
748
 
@@ -1036,7 +1073,7 @@ interface SanqianChatProps {
1036
1073
  className?: string;
1037
1074
  emptyState?: ReactNode;
1038
1075
  onError?: (error: Error) => void;
1039
- onConversationChange?: (conversationId: string, title?: string) => void;
1076
+ onConversationChange?: (conversationId: string, title?: string, meta?: ConversationChangeMeta) => void;
1040
1077
  onStateChange?: (state: {
1041
1078
  messages: ChatMessage[];
1042
1079
  conversationId: string | null;
@@ -1659,4 +1696,4 @@ declare function ensureChatBaseStyles(): void;
1659
1696
  */
1660
1697
  declare function ensureFullChatStyles(): void;
1661
1698
 
1662
- export { AddResourceButton, type AddResourceButtonProps, type AlertAction, AlertBanner, type AlertBannerProps, type AlertConfig, type AlertType, AttachButton, type AttachButtonProps, type AttachConfig, type AttachPosition, type AttachState, type AttachedResource, AttachedResourceTags, type AttachedResourceTagsProps, type AttachmentMenuItem, type AttachmentMenuItemType, type ChatAdapter, type ChatAdapterConfig, type ChatFontSize, ChatInput, type ChatInputHandle, type ChatInputProps, type ChatPanelConfig, type ChatPanelMode, type ChatPanelPosition, type ChatThemeMode, type ChatUiConfig, type ChatUiConfigSerializable, type ChatUiStrings, CompactChat, type CompactChatProps, type ConnectionErrorCode, type ConnectionStatus, type ContextProviderInfo, type ConversationDetail, type ConversationInfo, FloatingChat, type FloatingChatProps, type FloatingWindowConfig, HistoryList, type HistoryListProps, HistoryModal, type HistoryModalProps, HitlCard, type HitlCardProps, type HitlInterruptData, I18nProvider, type I18nProviderProps, IntermediateSteps, type IntermediateStepsProps, type LinkClickEvent, type LinkHandlerConfig, type Locale, MarkdownRenderer, type MarkdownRendererProps, type MessageBlock, MessageBubble, type MessageBubbleProps, MessageList, type MessageListProps, type MessageRole, ModeToggleButton, type ModeToggleButtonProps, PanelControls, type PanelControlsProps, PanelHeaderButtons, PanelResizer, Resizer, type ResizerProps, type ResolvedTheme, ResourceChip, ResourceChipList, type ResourceChipListProps, type ResourceChipProps, ResourcePicker, type ResourcePickerItem, type ResourcePickerProps, type ResourcePickerState, SanqianChat, SanqianChatMessage, type SanqianChatMessageProps, type SanqianChatProps, SanqianMessageList, type SanqianMessageListHandle, type SanqianMessageListProps, type SdkAdapterConfig, type SendMessage, type SendMessageOptions, type SessionResource, type SessionResourceEvent, type StoredSessionResource, type StreamEvent, StreamingTimeline, type StreamingTimelineProps, type ThemeMode, ThemeProvider, type ThemeProviderProps, ThinkingSection, type ThinkingSectionProps, ToolArgumentsDisplay, type ToolArgumentsDisplayProps, type ToolCall, type ToolCallStatus, type Translations, type UseAttachStateReturn, type UseChatOptions, type UseChatPanelReturn, type UseChatReturn, type UseConnectionOptions, type UseConnectionReturn, type UseConversationsOptions, type UseConversationsReturn, type UseFocusPersistenceOptions, type UseResourcePickerOptions, type UseResourcePickerReturn, type WindowPosition, createChatAdapter, createIpcAdapter, createSdkAdapter, ensureChatBaseStyles, ensureFullChatStyles, getTranslations, resolveChatStrings, useAttachState, useChat, useChatPanel, useChatStyles, useConnection, useConversations, useFocusPersistence, useI18n, useIpcUiConfig, useResolvedUiConfig, useResourcePicker, useStandaloneI18n, useStandaloneTheme, useTheme, useWindowDragLock };
1699
+ export { AddResourceButton, type AddResourceButtonProps, type AlertAction, AlertBanner, type AlertBannerProps, type AlertConfig, type AlertType, AttachButton, type AttachButtonProps, type AttachConfig, type AttachPosition, type AttachState, type AttachedResource, AttachedResourceTags, type AttachedResourceTagsProps, type AttachmentMenuItem, type AttachmentMenuItemType, type ChatAdapter, type ChatAdapterConfig, type ChatFontSize, ChatInput, type ChatInputHandle, type ChatInputProps, type ChatPanelConfig, type ChatPanelMode, type ChatPanelPosition, type ChatThemeMode, type ChatUiConfig, type ChatUiConfigSerializable, type ChatUiStrings, CompactChat, type CompactChatProps, type ConnectionErrorCode, type ConnectionStatus, type ContextProviderInfo, type ConversationChangeMeta, type ConversationDetail, type ConversationInfo, type ConversationSwitchOptions, FloatingChat, type FloatingChatProps, type FloatingWindowConfig, HistoryList, type HistoryListProps, HistoryModal, type HistoryModalProps, HitlCard, type HitlCardProps, type HitlInterruptData, I18nProvider, type I18nProviderProps, IntermediateSteps, type IntermediateStepsProps, type LinkClickEvent, type LinkHandlerConfig, type Locale, MarkdownRenderer, type MarkdownRendererProps, type MessageBlock, MessageBubble, type MessageBubbleProps, MessageList, type MessageListProps, type MessageRole, ModeToggleButton, type ModeToggleButtonProps, PanelControls, type PanelControlsProps, PanelHeaderButtons, PanelResizer, Resizer, type ResizerProps, type ResolvedTheme, ResourceChip, ResourceChipList, type ResourceChipListProps, type ResourceChipProps, ResourcePicker, type ResourcePickerItem, type ResourcePickerProps, type ResourcePickerState, SanqianChat, SanqianChatMessage, type SanqianChatMessageProps, type SanqianChatProps, SanqianMessageList, type SanqianMessageListHandle, type SanqianMessageListProps, type SdkAdapterConfig, type SendMessage, type SendMessageOptions, type SessionResource, type SessionResourceEvent, type StoredSessionResource, type StreamEvent, StreamingTimeline, type StreamingTimelineProps, type ThemeMode, ThemeProvider, type ThemeProviderProps, ThinkingSection, type ThinkingSectionProps, ToolArgumentsDisplay, type ToolArgumentsDisplayProps, type ToolCall, type ToolCallStatus, type Translations, type UseAttachStateReturn, type UseChatCapabilities, type UseChatOptions, type UseChatPanelReturn, type UseChatReturn, type UseConnectionOptions, type UseConnectionReturn, type UseConversationsOptions, type UseConversationsReturn, type UseFocusPersistenceOptions, type UseResourcePickerOptions, type UseResourcePickerReturn, type WindowPosition, createChatAdapter, createIpcAdapter, createSdkAdapter, ensureChatBaseStyles, ensureFullChatStyles, getTranslations, resolveChatStrings, useAttachState, useChat, useChatPanel, useChatStyles, useConnection, useConversations, useFocusPersistence, useI18n, useIpcUiConfig, useResolvedUiConfig, useResourcePicker, useStandaloneI18n, useStandaloneTheme, useTheme, useWindowDragLock };
@@ -677,13 +677,48 @@ interface UseChatOptions {
677
677
  adapter: ChatAdapter;
678
678
  conversationId?: string;
679
679
  onError?: (error: Error) => void;
680
- onConversationChange?: (conversationId: string, title?: string) => void;
680
+ onConversationChange?: (conversationId: string, title?: string, meta?: ConversationChangeMeta) => void;
681
681
  }
682
682
  /** Options for sendMessage */
683
683
  interface SendMessageOptions {
684
684
  /** Attached resources to include with the message */
685
685
  attachedResources?: AttachedResource[];
686
686
  }
687
+ interface ConversationChangeMeta {
688
+ /**
689
+ * Source of this conversation change event:
690
+ * - "active": current foreground stream completed and updated UI state
691
+ * - "background": a detached stream completed in background
692
+ */
693
+ source: 'active' | 'background';
694
+ /** Unique stream token within this hook instance */
695
+ streamToken?: string;
696
+ /** Whether this change comes from a detached/background stream */
697
+ detached?: boolean;
698
+ /** Optional user-provided context captured when detaching a stream */
699
+ detachContext?: unknown;
700
+ }
701
+ interface ConversationSwitchOptions {
702
+ /**
703
+ * Whether to cancel the currently active stream before switching.
704
+ * - true (default): existing behavior, stream is cancelled.
705
+ * - false: detach current stream and allow it to finish in background.
706
+ */
707
+ cancelActiveStream?: boolean;
708
+ /**
709
+ * Optional metadata persisted on detached stream context.
710
+ * Returned via onConversationChange meta when detached stream completes.
711
+ */
712
+ detachContext?: unknown;
713
+ }
714
+ interface UseChatCapabilities {
715
+ conversationSwitch: {
716
+ /** Supports `cancelActiveStream` option on loadConversation/newConversation */
717
+ supportsCancelActiveStream: true;
718
+ /** Supports `detachContext` propagation for detached/background streams */
719
+ supportsDetachContext: true;
720
+ };
721
+ }
687
722
  interface UseChatReturn {
688
723
  messages: ChatMessage[];
689
724
  isLoading: boolean;
@@ -700,12 +735,14 @@ interface UseChatReturn {
700
735
  approveHitl: (remember?: boolean) => void;
701
736
  rejectHitl: (remember?: boolean) => void;
702
737
  submitHitlInput: (response: HitlResponse) => void;
703
- loadConversation: (id: string) => Promise<void>;
704
- newConversation: () => void;
738
+ loadConversation: (id: string, options?: ConversationSwitchOptions) => Promise<void>;
739
+ newConversation: (options?: ConversationSwitchOptions) => void;
705
740
  /** Session resources pushed by apps (global, persists until app disconnects) */
706
741
  sessionResources: StoredSessionResource[];
707
742
  /** Remove a session resource (user action from UI) */
708
743
  removeSessionResource: (fullId: string) => void;
744
+ /** Feature flags for safe capability-based integrations */
745
+ capabilities?: UseChatCapabilities;
709
746
  }
710
747
  declare function useChat(options: UseChatOptions): UseChatReturn;
711
748
 
@@ -1036,7 +1073,7 @@ interface SanqianChatProps {
1036
1073
  className?: string;
1037
1074
  emptyState?: ReactNode;
1038
1075
  onError?: (error: Error) => void;
1039
- onConversationChange?: (conversationId: string, title?: string) => void;
1076
+ onConversationChange?: (conversationId: string, title?: string, meta?: ConversationChangeMeta) => void;
1040
1077
  onStateChange?: (state: {
1041
1078
  messages: ChatMessage[];
1042
1079
  conversationId: string | null;
@@ -1659,4 +1696,4 @@ declare function ensureChatBaseStyles(): void;
1659
1696
  */
1660
1697
  declare function ensureFullChatStyles(): void;
1661
1698
 
1662
- export { AddResourceButton, type AddResourceButtonProps, type AlertAction, AlertBanner, type AlertBannerProps, type AlertConfig, type AlertType, AttachButton, type AttachButtonProps, type AttachConfig, type AttachPosition, type AttachState, type AttachedResource, AttachedResourceTags, type AttachedResourceTagsProps, type AttachmentMenuItem, type AttachmentMenuItemType, type ChatAdapter, type ChatAdapterConfig, type ChatFontSize, ChatInput, type ChatInputHandle, type ChatInputProps, type ChatPanelConfig, type ChatPanelMode, type ChatPanelPosition, type ChatThemeMode, type ChatUiConfig, type ChatUiConfigSerializable, type ChatUiStrings, CompactChat, type CompactChatProps, type ConnectionErrorCode, type ConnectionStatus, type ContextProviderInfo, type ConversationDetail, type ConversationInfo, FloatingChat, type FloatingChatProps, type FloatingWindowConfig, HistoryList, type HistoryListProps, HistoryModal, type HistoryModalProps, HitlCard, type HitlCardProps, type HitlInterruptData, I18nProvider, type I18nProviderProps, IntermediateSteps, type IntermediateStepsProps, type LinkClickEvent, type LinkHandlerConfig, type Locale, MarkdownRenderer, type MarkdownRendererProps, type MessageBlock, MessageBubble, type MessageBubbleProps, MessageList, type MessageListProps, type MessageRole, ModeToggleButton, type ModeToggleButtonProps, PanelControls, type PanelControlsProps, PanelHeaderButtons, PanelResizer, Resizer, type ResizerProps, type ResolvedTheme, ResourceChip, ResourceChipList, type ResourceChipListProps, type ResourceChipProps, ResourcePicker, type ResourcePickerItem, type ResourcePickerProps, type ResourcePickerState, SanqianChat, SanqianChatMessage, type SanqianChatMessageProps, type SanqianChatProps, SanqianMessageList, type SanqianMessageListHandle, type SanqianMessageListProps, type SdkAdapterConfig, type SendMessage, type SendMessageOptions, type SessionResource, type SessionResourceEvent, type StoredSessionResource, type StreamEvent, StreamingTimeline, type StreamingTimelineProps, type ThemeMode, ThemeProvider, type ThemeProviderProps, ThinkingSection, type ThinkingSectionProps, ToolArgumentsDisplay, type ToolArgumentsDisplayProps, type ToolCall, type ToolCallStatus, type Translations, type UseAttachStateReturn, type UseChatOptions, type UseChatPanelReturn, type UseChatReturn, type UseConnectionOptions, type UseConnectionReturn, type UseConversationsOptions, type UseConversationsReturn, type UseFocusPersistenceOptions, type UseResourcePickerOptions, type UseResourcePickerReturn, type WindowPosition, createChatAdapter, createIpcAdapter, createSdkAdapter, ensureChatBaseStyles, ensureFullChatStyles, getTranslations, resolveChatStrings, useAttachState, useChat, useChatPanel, useChatStyles, useConnection, useConversations, useFocusPersistence, useI18n, useIpcUiConfig, useResolvedUiConfig, useResourcePicker, useStandaloneI18n, useStandaloneTheme, useTheme, useWindowDragLock };
1699
+ export { AddResourceButton, type AddResourceButtonProps, type AlertAction, AlertBanner, type AlertBannerProps, type AlertConfig, type AlertType, AttachButton, type AttachButtonProps, type AttachConfig, type AttachPosition, type AttachState, type AttachedResource, AttachedResourceTags, type AttachedResourceTagsProps, type AttachmentMenuItem, type AttachmentMenuItemType, type ChatAdapter, type ChatAdapterConfig, type ChatFontSize, ChatInput, type ChatInputHandle, type ChatInputProps, type ChatPanelConfig, type ChatPanelMode, type ChatPanelPosition, type ChatThemeMode, type ChatUiConfig, type ChatUiConfigSerializable, type ChatUiStrings, CompactChat, type CompactChatProps, type ConnectionErrorCode, type ConnectionStatus, type ContextProviderInfo, type ConversationChangeMeta, type ConversationDetail, type ConversationInfo, type ConversationSwitchOptions, FloatingChat, type FloatingChatProps, type FloatingWindowConfig, HistoryList, type HistoryListProps, HistoryModal, type HistoryModalProps, HitlCard, type HitlCardProps, type HitlInterruptData, I18nProvider, type I18nProviderProps, IntermediateSteps, type IntermediateStepsProps, type LinkClickEvent, type LinkHandlerConfig, type Locale, MarkdownRenderer, type MarkdownRendererProps, type MessageBlock, MessageBubble, type MessageBubbleProps, MessageList, type MessageListProps, type MessageRole, ModeToggleButton, type ModeToggleButtonProps, PanelControls, type PanelControlsProps, PanelHeaderButtons, PanelResizer, Resizer, type ResizerProps, type ResolvedTheme, ResourceChip, ResourceChipList, type ResourceChipListProps, type ResourceChipProps, ResourcePicker, type ResourcePickerItem, type ResourcePickerProps, type ResourcePickerState, SanqianChat, SanqianChatMessage, type SanqianChatMessageProps, type SanqianChatProps, SanqianMessageList, type SanqianMessageListHandle, type SanqianMessageListProps, type SdkAdapterConfig, type SendMessage, type SendMessageOptions, type SessionResource, type SessionResourceEvent, type StoredSessionResource, type StreamEvent, StreamingTimeline, type StreamingTimelineProps, type ThemeMode, ThemeProvider, type ThemeProviderProps, ThinkingSection, type ThinkingSectionProps, ToolArgumentsDisplay, type ToolArgumentsDisplayProps, type ToolCall, type ToolCallStatus, type Translations, type UseAttachStateReturn, type UseChatCapabilities, type UseChatOptions, type UseChatPanelReturn, type UseChatReturn, type UseConnectionOptions, type UseConnectionReturn, type UseConversationsOptions, type UseConversationsReturn, type UseFocusPersistenceOptions, type UseResourcePickerOptions, type UseResourcePickerReturn, type WindowPosition, createChatAdapter, createIpcAdapter, createSdkAdapter, ensureChatBaseStyles, ensureFullChatStyles, getTranslations, resolveChatStrings, useAttachState, useChat, useChatPanel, useChatStyles, useConnection, useConversations, useFocusPersistence, useI18n, useIpcUiConfig, useResolvedUiConfig, useResourcePicker, useStandaloneI18n, useStandaloneTheme, useTheme, useWindowDragLock };
@@ -107,6 +107,12 @@ var findLastBlock = (blocks, predicate) => {
107
107
  }
108
108
  return void 0;
109
109
  };
110
+ var CHAT_CAPABILITIES = {
111
+ conversationSwitch: {
112
+ supportsCancelActiveStream: true,
113
+ supportsDetachContext: true
114
+ }
115
+ };
110
116
  function useChat(options) {
111
117
  const { adapter, onError, onConversationChange } = options;
112
118
  const [messages, setMessages] = (0, import_react.useState)([]);
@@ -127,6 +133,7 @@ function useChat(options) {
127
133
  const pendingCancelRef = (0, import_react.useRef)(false);
128
134
  const pendingCancelFnRef = (0, import_react.useRef)(null);
129
135
  const suppressStreamRef = (0, import_react.useRef)(false);
136
+ const activeStreamContextRef = (0, import_react.useRef)(null);
130
137
  const currentBlocksRef = (0, import_react.useRef)([]);
131
138
  const currentTextBlockIndexRef = (0, import_react.useRef)(-1);
132
139
  const needsContentClearRef = (0, import_react.useRef)(false);
@@ -214,8 +221,43 @@ function useChat(options) {
214
221
  pendingCancelRef.current = false;
215
222
  pendingCancelFnRef.current = null;
216
223
  }, []);
217
- const handleStreamEvent = (0, import_react.useCallback)((event, assistantMessageId) => {
224
+ const detachActiveStream = (0, import_react.useCallback)((detachContext) => {
225
+ const context = activeStreamContextRef.current;
226
+ if (!context || context.detached) {
227
+ return;
228
+ }
229
+ context.detached = true;
230
+ context.detachContext = detachContext;
231
+ if (typewriterIntervalRef.current) {
232
+ clearTimeout(typewriterIntervalRef.current);
233
+ typewriterIntervalRef.current = null;
234
+ }
235
+ currentAssistantMessageIdRef.current = null;
236
+ resetStreamBuffers();
237
+ setIsStreaming(false);
238
+ setIsLoading(false);
239
+ setPendingInterrupt(null);
240
+ currentRunIdRef.current = null;
241
+ pendingInterruptStreamIdRef.current = null;
242
+ clearPendingCancel();
243
+ suppressStreamRef.current = false;
244
+ }, [clearPendingCancel, resetStreamBuffers]);
245
+ const handleStreamEvent = (0, import_react.useCallback)((event, streamContext) => {
218
246
  if (!isMountedRef.current) return;
247
+ const isActiveStream = activeStreamContextRef.current?.token === streamContext.token && !streamContext.detached;
248
+ if (!isActiveStream) {
249
+ if (event.type === "done" && event.conversationId && !streamContext.didReportConversationChange) {
250
+ streamContext.didReportConversationChange = true;
251
+ onConversationChange?.(event.conversationId, event.title, {
252
+ source: "background",
253
+ streamToken: streamContext.token,
254
+ detached: true,
255
+ detachContext: streamContext.detachContext
256
+ });
257
+ }
258
+ return;
259
+ }
260
+ const assistantMessageId = streamContext.assistantMessageId;
219
261
  currentAssistantMessageIdRef.current = assistantMessageId;
220
262
  if (suppressStreamRef.current && event.type !== "start" && event.type !== "cancelled" && event.type !== "done" && event.type !== "error") {
221
263
  return;
@@ -561,8 +603,13 @@ function useChat(options) {
561
603
  });
562
604
  resetStreamBuffers();
563
605
  if (event.conversationId) {
606
+ streamContext.didReportConversationChange = true;
564
607
  setConversationId(event.conversationId);
565
- onConversationChange?.(event.conversationId, event.title);
608
+ onConversationChange?.(event.conversationId, event.title, {
609
+ source: "active",
610
+ streamToken: streamContext.token,
611
+ detached: false
612
+ });
566
613
  }
567
614
  if (event.title) setConversationTitle(event.title);
568
615
  setIsStreaming(false);
@@ -571,6 +618,7 @@ function useChat(options) {
571
618
  pendingInterruptStreamIdRef.current = null;
572
619
  suppressStreamRef.current = false;
573
620
  clearPendingCancel();
621
+ activeStreamContextRef.current = null;
574
622
  break;
575
623
  }
576
624
  case "cancelled": {
@@ -608,6 +656,7 @@ function useChat(options) {
608
656
  pendingInterruptStreamIdRef.current = null;
609
657
  suppressStreamRef.current = false;
610
658
  clearPendingCancel();
659
+ activeStreamContextRef.current = null;
611
660
  break;
612
661
  }
613
662
  case "error": {
@@ -636,6 +685,7 @@ function useChat(options) {
636
685
  pendingInterruptStreamIdRef.current = null;
637
686
  suppressStreamRef.current = false;
638
687
  clearPendingCancel();
688
+ activeStreamContextRef.current = null;
639
689
  break;
640
690
  }
641
691
  case "interrupt": {
@@ -686,6 +736,13 @@ function useChat(options) {
686
736
  blocks: [],
687
737
  isComplete: false
688
738
  };
739
+ const streamContext = {
740
+ token: crypto.randomUUID(),
741
+ assistantMessageId: assistantMessage.id,
742
+ detached: false,
743
+ didReportConversationChange: false
744
+ };
745
+ activeStreamContextRef.current = streamContext;
689
746
  const shouldRenderUserMessage = trimmedContent.length > 0 || hasAttachedResources;
690
747
  setMessages(
691
748
  (prev) => shouldRenderUserMessage ? [...prev, userMessage, assistantMessage] : [...prev, assistantMessage]
@@ -708,7 +765,7 @@ function useChat(options) {
708
765
  const { cancel } = await adapter.chatStream(
709
766
  apiMessages,
710
767
  conversationIdRef.current ?? void 0,
711
- (event) => handleStreamEvent(event, assistantMessage.id),
768
+ (event) => handleStreamEvent(event, streamContext),
712
769
  {
713
770
  agentId: currentAgentIdRef.current,
714
771
  attachedResources: attachedResources?.length ? attachedResources : void 0,
@@ -743,6 +800,9 @@ function useChat(options) {
743
800
  pendingInterruptStreamIdRef.current = null;
744
801
  setIsLoading(false);
745
802
  setIsStreaming(false);
803
+ if (activeStreamContextRef.current?.token === streamContext.token) {
804
+ activeStreamContextRef.current = null;
805
+ }
746
806
  return false;
747
807
  }
748
808
  }, [adapter, clearPendingCancel, handleStreamEvent, onError, resetStreamBuffers, sessionResources]);
@@ -784,6 +844,7 @@ function useChat(options) {
784
844
  resetStreamBuffers();
785
845
  currentRunIdRef.current = null;
786
846
  pendingInterruptStreamIdRef.current = null;
847
+ activeStreamContextRef.current = null;
787
848
  setIsStreaming(false);
788
849
  setIsLoading(false);
789
850
  }, [clearPendingCancel, flushTypewriter, resetStreamBuffers]);
@@ -797,10 +858,20 @@ function useChat(options) {
797
858
  resetStreamBuffers();
798
859
  currentRunIdRef.current = null;
799
860
  pendingInterruptStreamIdRef.current = null;
861
+ activeStreamContextRef.current = null;
800
862
  clearPendingCancel();
801
863
  suppressStreamRef.current = false;
802
864
  }, [clearPendingCancel, resetStreamBuffers]);
803
- const loadConversation = (0, import_react.useCallback)(async (id) => {
865
+ const loadConversation = (0, import_react.useCallback)(async (id, optionsArg) => {
866
+ const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
867
+ const activeStream = activeStreamContextRef.current;
868
+ if (activeStream && !activeStream.detached) {
869
+ if (cancelActiveStream) {
870
+ stopStreaming();
871
+ } else {
872
+ detachActiveStream(optionsArg?.detachContext);
873
+ }
874
+ }
804
875
  try {
805
876
  setIsLoading(true);
806
877
  setError(null);
@@ -821,9 +892,19 @@ function useChat(options) {
821
892
  } finally {
822
893
  if (isMountedRef.current) setIsLoading(false);
823
894
  }
824
- }, [adapter, onError]);
825
- const newConversation = (0, import_react.useCallback)(() => {
826
- cancelRef.current?.();
895
+ }, [adapter, detachActiveStream, onError, stopStreaming]);
896
+ const newConversation = (0, import_react.useCallback)((optionsArg) => {
897
+ const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
898
+ const activeStream = activeStreamContextRef.current;
899
+ if (activeStream && !activeStream.detached) {
900
+ if (cancelActiveStream) {
901
+ stopStreaming();
902
+ } else {
903
+ detachActiveStream(optionsArg?.detachContext);
904
+ }
905
+ } else if (cancelActiveStream) {
906
+ cancelRef.current?.();
907
+ }
827
908
  if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
828
909
  setMessages([]);
829
910
  setConversationId(null);
@@ -835,8 +916,9 @@ function useChat(options) {
835
916
  resetStreamBuffers();
836
917
  currentRunIdRef.current = null;
837
918
  pendingInterruptStreamIdRef.current = null;
919
+ activeStreamContextRef.current = null;
838
920
  clearPendingCancel();
839
- }, [clearPendingCancel, resetStreamBuffers]);
921
+ }, [clearPendingCancel, detachActiveStream, resetStreamBuffers, stopStreaming]);
840
922
  const sendHitlResponse = (0, import_react.useCallback)((response) => {
841
923
  const runId = currentRunIdRef.current ?? void 0;
842
924
  const streamId = pendingInterruptStreamIdRef.current ?? void 0;
@@ -916,7 +998,8 @@ function useChat(options) {
916
998
  loadConversation,
917
999
  newConversation,
918
1000
  sessionResources,
919
- removeSessionResource
1001
+ removeSessionResource,
1002
+ capabilities: CHAT_CAPABILITIES
920
1003
  };
921
1004
  }
922
1005
 
@@ -6951,8 +7034,7 @@ var MarkdownRenderer = (0, import_react18.memo)(function MarkdownRenderer2({
6951
7034
  defaultOrigin: origin,
6952
7035
  allowDataImages: true
6953
7036
  }],
6954
- import_streamdown.defaultRehypePlugins.raw,
6955
- import_streamdown.defaultRehypePlugins.katex
7037
+ import_streamdown.defaultRehypePlugins.raw
6956
7038
  ];
6957
7039
  }, [allowedProtocols]);
6958
7040
  const handleLinkClick = (0, import_react18.useCallback)((href, event) => {
@@ -6974,7 +7056,7 @@ var MarkdownRenderer = (0, import_react18.memo)(function MarkdownRenderer2({
6974
7056
  }
6975
7057
  }, [linkHandler, onLinkClick]);
6976
7058
  const remarkPlugins = (0, import_react18.useMemo)(() => {
6977
- return [import_remark_gfm.default, import_streamdown.defaultRemarkPlugins.math];
7059
+ return [import_remark_gfm.default];
6978
7060
  }, []);
6979
7061
  const customComponents = (0, import_react18.useMemo)(() => {
6980
7062
  const comps = {};
@@ -19,6 +19,12 @@ var findLastBlock = (blocks, predicate) => {
19
19
  }
20
20
  return void 0;
21
21
  };
22
+ var CHAT_CAPABILITIES = {
23
+ conversationSwitch: {
24
+ supportsCancelActiveStream: true,
25
+ supportsDetachContext: true
26
+ }
27
+ };
22
28
  function useChat(options) {
23
29
  const { adapter, onError, onConversationChange } = options;
24
30
  const [messages, setMessages] = useState([]);
@@ -39,6 +45,7 @@ function useChat(options) {
39
45
  const pendingCancelRef = useRef(false);
40
46
  const pendingCancelFnRef = useRef(null);
41
47
  const suppressStreamRef = useRef(false);
48
+ const activeStreamContextRef = useRef(null);
42
49
  const currentBlocksRef = useRef([]);
43
50
  const currentTextBlockIndexRef = useRef(-1);
44
51
  const needsContentClearRef = useRef(false);
@@ -126,8 +133,43 @@ function useChat(options) {
126
133
  pendingCancelRef.current = false;
127
134
  pendingCancelFnRef.current = null;
128
135
  }, []);
129
- const handleStreamEvent = useCallback((event, assistantMessageId) => {
136
+ const detachActiveStream = useCallback((detachContext) => {
137
+ const context = activeStreamContextRef.current;
138
+ if (!context || context.detached) {
139
+ return;
140
+ }
141
+ context.detached = true;
142
+ context.detachContext = detachContext;
143
+ if (typewriterIntervalRef.current) {
144
+ clearTimeout(typewriterIntervalRef.current);
145
+ typewriterIntervalRef.current = null;
146
+ }
147
+ currentAssistantMessageIdRef.current = null;
148
+ resetStreamBuffers();
149
+ setIsStreaming(false);
150
+ setIsLoading(false);
151
+ setPendingInterrupt(null);
152
+ currentRunIdRef.current = null;
153
+ pendingInterruptStreamIdRef.current = null;
154
+ clearPendingCancel();
155
+ suppressStreamRef.current = false;
156
+ }, [clearPendingCancel, resetStreamBuffers]);
157
+ const handleStreamEvent = useCallback((event, streamContext) => {
130
158
  if (!isMountedRef.current) return;
159
+ const isActiveStream = activeStreamContextRef.current?.token === streamContext.token && !streamContext.detached;
160
+ if (!isActiveStream) {
161
+ if (event.type === "done" && event.conversationId && !streamContext.didReportConversationChange) {
162
+ streamContext.didReportConversationChange = true;
163
+ onConversationChange?.(event.conversationId, event.title, {
164
+ source: "background",
165
+ streamToken: streamContext.token,
166
+ detached: true,
167
+ detachContext: streamContext.detachContext
168
+ });
169
+ }
170
+ return;
171
+ }
172
+ const assistantMessageId = streamContext.assistantMessageId;
131
173
  currentAssistantMessageIdRef.current = assistantMessageId;
132
174
  if (suppressStreamRef.current && event.type !== "start" && event.type !== "cancelled" && event.type !== "done" && event.type !== "error") {
133
175
  return;
@@ -473,8 +515,13 @@ function useChat(options) {
473
515
  });
474
516
  resetStreamBuffers();
475
517
  if (event.conversationId) {
518
+ streamContext.didReportConversationChange = true;
476
519
  setConversationId(event.conversationId);
477
- onConversationChange?.(event.conversationId, event.title);
520
+ onConversationChange?.(event.conversationId, event.title, {
521
+ source: "active",
522
+ streamToken: streamContext.token,
523
+ detached: false
524
+ });
478
525
  }
479
526
  if (event.title) setConversationTitle(event.title);
480
527
  setIsStreaming(false);
@@ -483,6 +530,7 @@ function useChat(options) {
483
530
  pendingInterruptStreamIdRef.current = null;
484
531
  suppressStreamRef.current = false;
485
532
  clearPendingCancel();
533
+ activeStreamContextRef.current = null;
486
534
  break;
487
535
  }
488
536
  case "cancelled": {
@@ -520,6 +568,7 @@ function useChat(options) {
520
568
  pendingInterruptStreamIdRef.current = null;
521
569
  suppressStreamRef.current = false;
522
570
  clearPendingCancel();
571
+ activeStreamContextRef.current = null;
523
572
  break;
524
573
  }
525
574
  case "error": {
@@ -548,6 +597,7 @@ function useChat(options) {
548
597
  pendingInterruptStreamIdRef.current = null;
549
598
  suppressStreamRef.current = false;
550
599
  clearPendingCancel();
600
+ activeStreamContextRef.current = null;
551
601
  break;
552
602
  }
553
603
  case "interrupt": {
@@ -598,6 +648,13 @@ function useChat(options) {
598
648
  blocks: [],
599
649
  isComplete: false
600
650
  };
651
+ const streamContext = {
652
+ token: crypto.randomUUID(),
653
+ assistantMessageId: assistantMessage.id,
654
+ detached: false,
655
+ didReportConversationChange: false
656
+ };
657
+ activeStreamContextRef.current = streamContext;
601
658
  const shouldRenderUserMessage = trimmedContent.length > 0 || hasAttachedResources;
602
659
  setMessages(
603
660
  (prev) => shouldRenderUserMessage ? [...prev, userMessage, assistantMessage] : [...prev, assistantMessage]
@@ -620,7 +677,7 @@ function useChat(options) {
620
677
  const { cancel } = await adapter.chatStream(
621
678
  apiMessages,
622
679
  conversationIdRef.current ?? void 0,
623
- (event) => handleStreamEvent(event, assistantMessage.id),
680
+ (event) => handleStreamEvent(event, streamContext),
624
681
  {
625
682
  agentId: currentAgentIdRef.current,
626
683
  attachedResources: attachedResources?.length ? attachedResources : void 0,
@@ -655,6 +712,9 @@ function useChat(options) {
655
712
  pendingInterruptStreamIdRef.current = null;
656
713
  setIsLoading(false);
657
714
  setIsStreaming(false);
715
+ if (activeStreamContextRef.current?.token === streamContext.token) {
716
+ activeStreamContextRef.current = null;
717
+ }
658
718
  return false;
659
719
  }
660
720
  }, [adapter, clearPendingCancel, handleStreamEvent, onError, resetStreamBuffers, sessionResources]);
@@ -696,6 +756,7 @@ function useChat(options) {
696
756
  resetStreamBuffers();
697
757
  currentRunIdRef.current = null;
698
758
  pendingInterruptStreamIdRef.current = null;
759
+ activeStreamContextRef.current = null;
699
760
  setIsStreaming(false);
700
761
  setIsLoading(false);
701
762
  }, [clearPendingCancel, flushTypewriter, resetStreamBuffers]);
@@ -709,10 +770,20 @@ function useChat(options) {
709
770
  resetStreamBuffers();
710
771
  currentRunIdRef.current = null;
711
772
  pendingInterruptStreamIdRef.current = null;
773
+ activeStreamContextRef.current = null;
712
774
  clearPendingCancel();
713
775
  suppressStreamRef.current = false;
714
776
  }, [clearPendingCancel, resetStreamBuffers]);
715
- const loadConversation = useCallback(async (id) => {
777
+ const loadConversation = useCallback(async (id, optionsArg) => {
778
+ const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
779
+ const activeStream = activeStreamContextRef.current;
780
+ if (activeStream && !activeStream.detached) {
781
+ if (cancelActiveStream) {
782
+ stopStreaming();
783
+ } else {
784
+ detachActiveStream(optionsArg?.detachContext);
785
+ }
786
+ }
716
787
  try {
717
788
  setIsLoading(true);
718
789
  setError(null);
@@ -733,9 +804,19 @@ function useChat(options) {
733
804
  } finally {
734
805
  if (isMountedRef.current) setIsLoading(false);
735
806
  }
736
- }, [adapter, onError]);
737
- const newConversation = useCallback(() => {
738
- cancelRef.current?.();
807
+ }, [adapter, detachActiveStream, onError, stopStreaming]);
808
+ const newConversation = useCallback((optionsArg) => {
809
+ const cancelActiveStream = optionsArg?.cancelActiveStream ?? true;
810
+ const activeStream = activeStreamContextRef.current;
811
+ if (activeStream && !activeStream.detached) {
812
+ if (cancelActiveStream) {
813
+ stopStreaming();
814
+ } else {
815
+ detachActiveStream(optionsArg?.detachContext);
816
+ }
817
+ } else if (cancelActiveStream) {
818
+ cancelRef.current?.();
819
+ }
739
820
  if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
740
821
  setMessages([]);
741
822
  setConversationId(null);
@@ -747,8 +828,9 @@ function useChat(options) {
747
828
  resetStreamBuffers();
748
829
  currentRunIdRef.current = null;
749
830
  pendingInterruptStreamIdRef.current = null;
831
+ activeStreamContextRef.current = null;
750
832
  clearPendingCancel();
751
- }, [clearPendingCancel, resetStreamBuffers]);
833
+ }, [clearPendingCancel, detachActiveStream, resetStreamBuffers, stopStreaming]);
752
834
  const sendHitlResponse = useCallback((response) => {
753
835
  const runId = currentRunIdRef.current ?? void 0;
754
836
  const streamId = pendingInterruptStreamIdRef.current ?? void 0;
@@ -828,7 +910,8 @@ function useChat(options) {
828
910
  loadConversation,
829
911
  newConversation,
830
912
  sessionResources,
831
- removeSessionResource
913
+ removeSessionResource,
914
+ capabilities: CHAT_CAPABILITIES
832
915
  };
833
916
  }
834
917
 
@@ -6836,7 +6919,7 @@ import { memo as memo6 } from "react";
6836
6919
 
6837
6920
  // src/renderer/renderers/MarkdownRenderer.tsx
6838
6921
  import { memo as memo3, useMemo as useMemo5, useCallback as useCallback11 } from "react";
6839
- import { Streamdown, defaultRehypePlugins, defaultRemarkPlugins } from "streamdown";
6922
+ import { Streamdown, defaultRehypePlugins } from "streamdown";
6840
6923
  import { harden } from "rehype-harden";
6841
6924
  import remarkGfm from "remark-gfm";
6842
6925
  import { jsx as jsx6 } from "react/jsx-runtime";
@@ -6871,8 +6954,7 @@ var MarkdownRenderer = memo3(function MarkdownRenderer2({
6871
6954
  defaultOrigin: origin,
6872
6955
  allowDataImages: true
6873
6956
  }],
6874
- defaultRehypePlugins.raw,
6875
- defaultRehypePlugins.katex
6957
+ defaultRehypePlugins.raw
6876
6958
  ];
6877
6959
  }, [allowedProtocols]);
6878
6960
  const handleLinkClick = useCallback11((href, event) => {
@@ -6894,7 +6976,7 @@ var MarkdownRenderer = memo3(function MarkdownRenderer2({
6894
6976
  }
6895
6977
  }, [linkHandler, onLinkClick]);
6896
6978
  const remarkPlugins = useMemo5(() => {
6897
- return [remarkGfm, defaultRemarkPlugins.math];
6979
+ return [remarkGfm];
6898
6980
  }, []);
6899
6981
  const customComponents = useMemo5(() => {
6900
6982
  const comps = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yushaw/sanqian-chat",
3
- "version": "0.2.36",
3
+ "version": "0.2.39",
4
4
  "description": "Floating chat window SDK for Sanqian AI Assistant",
5
5
  "main": "./dist/main/index.js",
6
6
  "types": "./dist/main/index.d.ts",