lemma-sdk 0.2.32 → 0.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +190 -54
  2. package/dist/browser/lemma-client.js +84 -14
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +2 -1
  5. package/dist/namespaces/desks.d.ts +5 -2
  6. package/dist/namespaces/desks.js +5 -2
  7. package/dist/namespaces/files.d.ts +11 -0
  8. package/dist/namespaces/files.js +12 -0
  9. package/dist/namespaces/pod-surfaces.js +1 -1
  10. package/dist/namespaces/workflows.d.ts +1 -1
  11. package/dist/openapi_client/index.d.ts +50 -12
  12. package/dist/openapi_client/index.js +10 -2
  13. package/dist/openapi_client/models/AdminConsentInfoResponse.d.ts +6 -0
  14. package/dist/openapi_client/models/AssistantSurfaceResponse.d.ts +16 -1
  15. package/dist/openapi_client/models/AssistantSurfaceStatus.d.ts +5 -0
  16. package/dist/openapi_client/models/AssistantSurfaceStatus.js +10 -0
  17. package/dist/openapi_client/models/ConversationMessageResponse.d.ts +4 -0
  18. package/dist/openapi_client/models/ConversationResponse.d.ts +3 -0
  19. package/dist/openapi_client/models/ConversationType.d.ts +8 -0
  20. package/dist/openapi_client/models/ConversationType.js +13 -0
  21. package/dist/openapi_client/models/CreateSurfaceRequest.d.ts +16 -5
  22. package/dist/openapi_client/models/{DataStoreFlowStart.d.ts → DataStoreFlowStartInput.d.ts} +1 -1
  23. package/dist/openapi_client/models/DataStoreFlowStartOutput.d.ts +11 -0
  24. package/dist/openapi_client/models/DataStoreWorkflowStartInput.d.ts +11 -0
  25. package/dist/openapi_client/models/DataStoreWorkflowStartOutput.d.ts +11 -0
  26. package/dist/openapi_client/models/DeskBundleUploadRequest.d.ts +1 -1
  27. package/dist/openapi_client/models/{EventFlowStart.d.ts → EventFlowStartInput.d.ts} +1 -1
  28. package/dist/openapi_client/models/EventFlowStartOutput.d.ts +19 -0
  29. package/dist/openapi_client/models/EventWorkflowStartInput.d.ts +11 -0
  30. package/dist/openapi_client/models/EventWorkflowStartOutput.d.ts +11 -0
  31. package/dist/openapi_client/models/FlowInstallResponse.d.ts +15 -0
  32. package/dist/openapi_client/models/FlowResponse.d.ts +5 -2
  33. package/dist/openapi_client/models/GmailSurfaceConfig.d.ts +7 -0
  34. package/dist/openapi_client/models/GmailSurfaceConfig.js +1 -0
  35. package/dist/openapi_client/models/GmailSurfaceConfigInput.d.ts +4 -0
  36. package/dist/openapi_client/models/GmailSurfaceConfigInput.js +1 -0
  37. package/dist/openapi_client/models/GrantPermissionRequest.d.ts +6 -0
  38. package/dist/openapi_client/models/GrantPermissionRequest.js +1 -0
  39. package/dist/openapi_client/models/ManualWorkflowStartInput.d.ts +10 -0
  40. package/dist/openapi_client/models/ManualWorkflowStartInput.js +1 -0
  41. package/dist/openapi_client/models/ManualWorkflowStartOutput.d.ts +10 -0
  42. package/dist/openapi_client/models/ManualWorkflowStartOutput.js +1 -0
  43. package/dist/openapi_client/models/OrganizationInvitationRequest.d.ts +2 -0
  44. package/dist/openapi_client/models/OrganizationInvitationResponse.d.ts +2 -0
  45. package/dist/openapi_client/models/OutlookSurfaceConfig.d.ts +7 -0
  46. package/dist/openapi_client/models/OutlookSurfaceConfig.js +1 -0
  47. package/dist/openapi_client/models/OutlookSurfaceConfigInput.d.ts +4 -0
  48. package/dist/openapi_client/models/OutlookSurfaceConfigInput.js +1 -0
  49. package/dist/openapi_client/models/ResourcePermissionListResponse.d.ts +4 -0
  50. package/dist/openapi_client/models/ResourcePermissionListResponse.js +1 -0
  51. package/dist/openapi_client/models/ResourcePermissionResponse.d.ts +12 -0
  52. package/dist/openapi_client/models/ResourcePermissionResponse.js +1 -0
  53. package/dist/openapi_client/models/{ScheduledFlowStart.d.ts → ScheduledFlowStartInput.d.ts} +1 -1
  54. package/dist/openapi_client/models/ScheduledFlowStartInput.js +1 -0
  55. package/dist/openapi_client/models/ScheduledFlowStartOutput.d.ts +7 -0
  56. package/dist/openapi_client/models/ScheduledFlowStartOutput.js +1 -0
  57. package/dist/openapi_client/models/ScheduledWorkflowStartInput.d.ts +11 -0
  58. package/dist/openapi_client/models/ScheduledWorkflowStartInput.js +1 -0
  59. package/dist/openapi_client/models/ScheduledWorkflowStartOutput.d.ts +11 -0
  60. package/dist/openapi_client/models/ScheduledWorkflowStartOutput.js +1 -0
  61. package/dist/openapi_client/models/SetVisibilityRequest.d.ts +3 -0
  62. package/dist/openapi_client/models/SetVisibilityRequest.js +1 -0
  63. package/dist/openapi_client/models/SlackCredentialsInput.d.ts +5 -0
  64. package/dist/openapi_client/models/SlackCredentialsInput.js +1 -0
  65. package/dist/openapi_client/models/SlackSurfaceConfig.d.ts +10 -0
  66. package/dist/openapi_client/models/SlackSurfaceConfig.js +1 -0
  67. package/dist/openapi_client/models/{SlackSurfaceConfigCreate.d.ts → SlackSurfaceConfigInput.d.ts} +1 -1
  68. package/dist/openapi_client/models/SlackSurfaceConfigInput.js +1 -0
  69. package/dist/openapi_client/models/SurfaceCredentialMode.d.ts +4 -0
  70. package/dist/openapi_client/models/SurfaceCredentialMode.js +9 -0
  71. package/dist/openapi_client/models/SurfaceIntegrationSetupGuide.d.ts +13 -0
  72. package/dist/openapi_client/models/SurfaceIntegrationSetupGuide.js +1 -0
  73. package/dist/openapi_client/models/SurfacePlatform.d.ts +8 -0
  74. package/dist/openapi_client/models/SurfacePlatform.js +13 -0
  75. package/dist/openapi_client/models/SurfacePlatformSetupGuideResponse.d.ts +9 -0
  76. package/dist/openapi_client/models/SurfacePlatformSetupGuideResponse.js +1 -0
  77. package/dist/openapi_client/models/SurfaceRoutingScope.d.ts +4 -0
  78. package/dist/openapi_client/models/SurfaceRoutingScope.js +9 -0
  79. package/dist/openapi_client/models/SurfaceSetupField.d.ts +10 -0
  80. package/dist/openapi_client/models/SurfaceSetupField.js +1 -0
  81. package/dist/openapi_client/models/SurfaceSetupFieldSource.d.ts +4 -0
  82. package/dist/openapi_client/models/SurfaceSetupFieldSource.js +9 -0
  83. package/dist/openapi_client/models/SurfaceSetupMode.d.ts +5 -0
  84. package/dist/openapi_client/models/SurfaceSetupMode.js +10 -0
  85. package/dist/openapi_client/models/SurfaceSetupPhase.d.ts +6 -0
  86. package/dist/openapi_client/models/SurfaceSetupPhase.js +11 -0
  87. package/dist/openapi_client/models/SurfaceSetupStep.d.ts +6 -0
  88. package/dist/openapi_client/models/SurfaceSetupStep.js +1 -0
  89. package/dist/openapi_client/models/SurfaceWebhookMode.d.ts +4 -0
  90. package/dist/openapi_client/models/SurfaceWebhookMode.js +9 -0
  91. package/dist/openapi_client/models/TeamsCredentialsInput.d.ts +6 -0
  92. package/dist/openapi_client/models/TeamsCredentialsInput.js +1 -0
  93. package/dist/openapi_client/models/TeamsSurfaceConfig.d.ts +9 -0
  94. package/dist/openapi_client/models/TeamsSurfaceConfig.js +1 -0
  95. package/dist/openapi_client/models/{TeamsSurfaceConfigCreate.d.ts → TeamsSurfaceConfigInput.d.ts} +1 -1
  96. package/dist/openapi_client/models/TeamsSurfaceConfigInput.js +1 -0
  97. package/dist/openapi_client/models/TelegramCredentialsInput.d.ts +5 -0
  98. package/dist/openapi_client/models/TelegramCredentialsInput.js +1 -0
  99. package/dist/openapi_client/models/TelegramSurfaceConfig.d.ts +6 -0
  100. package/dist/openapi_client/models/TelegramSurfaceConfig.js +1 -0
  101. package/dist/openapi_client/models/TelegramSurfaceConfigInput.d.ts +6 -0
  102. package/dist/openapi_client/models/TelegramSurfaceConfigInput.js +1 -0
  103. package/dist/openapi_client/models/UpdateSurfaceRequest.d.ts +17 -5
  104. package/dist/openapi_client/models/WhatsAppCredentialsInput.d.ts +8 -0
  105. package/dist/openapi_client/models/WhatsAppCredentialsInput.js +1 -0
  106. package/dist/openapi_client/models/WhatsAppSurfaceConfig.d.ts +6 -0
  107. package/dist/openapi_client/models/WhatsAppSurfaceConfig.js +1 -0
  108. package/dist/openapi_client/models/WhatsAppSurfaceConfigInput.d.ts +6 -0
  109. package/dist/openapi_client/models/WhatsAppSurfaceConfigInput.js +1 -0
  110. package/dist/openapi_client/models/WorkflowCreateRequest.d.ts +5 -2
  111. package/dist/openapi_client/models/WorkflowGraphUpdateRequest.d.ts +5 -2
  112. package/dist/openapi_client/models/WorkflowUpdateRequest.d.ts +5 -2
  113. package/dist/openapi_client/services/AssistantSurfacesIngressService.d.ts +51 -0
  114. package/dist/openapi_client/services/AssistantSurfacesIngressService.js +111 -0
  115. package/dist/openapi_client/services/AssistantSurfacesService.d.ts +27 -6
  116. package/dist/openapi_client/services/AssistantSurfacesService.js +62 -7
  117. package/dist/openapi_client/services/AuthorizationService.d.ts +47 -0
  118. package/dist/openapi_client/services/AuthorizationService.js +100 -0
  119. package/dist/openapi_client/services/DesksService.d.ts +2 -2
  120. package/dist/openapi_client/services/DesksService.js +3 -3
  121. package/dist/openapi_client/services/WorkflowsService.d.ts +3 -3
  122. package/dist/openapi_client/services/WorkflowsService.js +1 -1
  123. package/dist/react/index.d.ts +16 -0
  124. package/dist/react/index.js +8 -0
  125. package/dist/react/useAddPodMember.d.ts +22 -0
  126. package/dist/react/useAddPodMember.js +51 -0
  127. package/dist/react/useAssistantController.js +82 -37
  128. package/dist/react/useAssistantRuntime.js +8 -4
  129. package/dist/react/useAssistantSession.js +44 -2
  130. package/dist/react/useConversationMessages.js +19 -2
  131. package/dist/react/useFile.d.ts +18 -0
  132. package/dist/react/useFile.js +58 -0
  133. package/dist/react/useFilePreview.d.ts +23 -0
  134. package/dist/react/useFilePreview.js +76 -0
  135. package/dist/react/useFileSearch.d.ts +26 -0
  136. package/dist/react/useFileSearch.js +64 -0
  137. package/dist/react/useFileTree.d.ts +21 -0
  138. package/dist/react/useFileTree.js +59 -0
  139. package/dist/react/useFiles.d.ts +29 -0
  140. package/dist/react/useFiles.js +90 -0
  141. package/dist/react/useRecords.js +8 -5
  142. package/dist/react/useRemovePodMember.d.ts +19 -0
  143. package/dist/react/useRemovePodMember.js +50 -0
  144. package/dist/react/useUpdatePodMemberRole.d.ts +20 -0
  145. package/dist/react/useUpdatePodMemberRole.js +50 -0
  146. package/dist/react/useWorkflowStart.d.ts +2 -2
  147. package/dist/record-display.d.ts +20 -0
  148. package/dist/record-display.js +78 -0
  149. package/dist/record-form.d.ts +4 -0
  150. package/dist/record-form.js +16 -0
  151. package/dist/types.d.ts +8 -1
  152. package/package.json +2 -1
  153. package/dist/openapi_client/models/EmailSurfaceConfigCreate.d.ts +0 -5
  154. package/dist/openapi_client/models/FlowInstallEntity.d.ts +0 -16
  155. package/dist/openapi_client/models/FlowStartType.d.ts +0 -6
  156. package/dist/openapi_client/models/FlowStartType.js +0 -11
  157. package/dist/openapi_client/models/FlowStart_Input.d.ts +0 -14
  158. package/dist/openapi_client/models/FlowStart_Output.d.ts +0 -14
  159. package/dist/openapi_client/models/WhatsAppSurfaceConfigCreate.d.ts +0 -5
  160. package/dist/openapi_client/services/PublicDesksService.d.ts +0 -10
  161. package/dist/openapi_client/services/PublicDesksService.js +0 -22
  162. /package/dist/openapi_client/models/{DataStoreFlowStart.js → AdminConsentInfoResponse.js} +0 -0
  163. /package/dist/openapi_client/models/{EmailSurfaceConfigCreate.js → DataStoreFlowStartInput.js} +0 -0
  164. /package/dist/openapi_client/models/{EventFlowStart.js → DataStoreFlowStartOutput.js} +0 -0
  165. /package/dist/openapi_client/models/{FlowInstallEntity.js → DataStoreWorkflowStartInput.js} +0 -0
  166. /package/dist/openapi_client/models/{FlowStart_Input.js → DataStoreWorkflowStartOutput.js} +0 -0
  167. /package/dist/openapi_client/models/{FlowStart_Output.js → EventFlowStartInput.js} +0 -0
  168. /package/dist/openapi_client/models/{ScheduledFlowStart.js → EventFlowStartOutput.js} +0 -0
  169. /package/dist/openapi_client/models/{SlackSurfaceConfigCreate.js → EventWorkflowStartInput.js} +0 -0
  170. /package/dist/openapi_client/models/{TeamsSurfaceConfigCreate.js → EventWorkflowStartOutput.js} +0 -0
  171. /package/dist/openapi_client/models/{WhatsAppSurfaceConfigCreate.js → FlowInstallResponse.js} +0 -0
@@ -541,7 +541,6 @@ function isConversationRunning(status) {
541
541
  }
542
542
  export function useAssistantController({ client, podId, assistantName, assistantId, organizationId, enabled = true, }) {
543
543
  const [localError, setLocalError] = useState(null);
544
- const [messages, setMessages] = useState([]);
545
544
  const [conversations, setConversations] = useState([]);
546
545
  const [activeConversationId, setActiveConversationId] = useState(null);
547
546
  const [availableModels, setAvailableModels] = useState([]);
@@ -555,10 +554,14 @@ export function useAssistantController({ client, podId, assistantName, assistant
555
554
  const [olderMessagesCursor, setOlderMessagesCursor] = useState(null);
556
555
  const activeConversationIdRef = useRef(null);
557
556
  const conversationsRef = useRef([]);
557
+ const isStreamingRef = useRef(false);
558
+ const sessionIsStreamingRef = useRef(false);
558
559
  const suppressAutoSelectRef = useRef(false);
559
560
  const lastAutoLoadedConversationIdRef = useRef(null);
560
561
  const loadingConversationIdRef = useRef(null);
561
562
  const skipInitialLoadConversationIdsRef = useRef(new Set());
563
+ const loadConversationMessagesRef = useRef(null);
564
+ const resumeIfRunningRef = useRef(null);
562
565
  const scope = useMemo(() => ({
563
566
  podId: podId ?? null,
564
567
  assistantName: assistantName ?? assistantId ?? null,
@@ -715,12 +718,24 @@ export function useAssistantController({ client, podId, assistantName, assistant
715
718
  setIsLoadingOlderMessages(false);
716
719
  }
717
720
  }, [isLoadingMessages, isLoadingOlderMessages, mergeMessages, olderMessagesCursor, sessionLoadMessages]);
721
+ useEffect(() => {
722
+ loadConversationMessagesRef.current = loadConversationMessages;
723
+ }, [loadConversationMessages]);
724
+ useEffect(() => {
725
+ resumeIfRunningRef.current = sessionResumeIfRunning;
726
+ }, [sessionResumeIfRunning]);
718
727
  useEffect(() => {
719
728
  activeConversationIdRef.current = activeConversationId;
720
729
  }, [activeConversationId]);
721
730
  useEffect(() => {
722
731
  conversationsRef.current = conversations;
723
732
  }, [conversations]);
733
+ useEffect(() => {
734
+ isStreamingRef.current = isStreaming;
735
+ }, [isStreaming]);
736
+ useEffect(() => {
737
+ sessionIsStreamingRef.current = sessionIsStreaming;
738
+ }, [sessionIsStreaming]);
724
739
  useEffect(() => {
725
740
  if (!enabled) {
726
741
  setAvailableModels([]);
@@ -738,22 +753,17 @@ export function useAssistantController({ client, podId, assistantName, assistant
738
753
  cancelled = true;
739
754
  };
740
755
  }, [enabled, loadAvailableModels]);
741
- useEffect(() => {
742
- const conversationId = activeConversationIdRef.current;
743
- if (!conversationId) {
744
- setMessages([]);
745
- return;
746
- }
756
+ const messages = useMemo(() => {
757
+ if (!activeConversationId)
758
+ return [];
747
759
  const normalized = sortMessagesByCreatedAt(runtimeMessages)
748
- .filter((message) => message.conversation_id === conversationId);
749
- if (normalized.length === 0) {
750
- setMessages([]);
751
- return;
752
- }
760
+ .filter((message) => message.conversation_id === activeConversationId);
761
+ if (normalized.length === 0)
762
+ return [];
753
763
  const nextMessages = mapConversationMessages(normalized);
754
764
  const pendingText = sessionStreamingText.trim();
755
765
  if (pendingText.length > 0) {
756
- const streamingId = `streaming-${conversationId}`;
766
+ const streamingId = `streaming-${activeConversationId}`;
757
767
  nextMessages.push({
758
768
  id: streamingId,
759
769
  role: "assistant",
@@ -762,8 +772,8 @@ export function useAssistantController({ client, podId, assistantName, assistant
762
772
  parts: [{ id: `${streamingId}-text`, type: "text", text: pendingText }],
763
773
  });
764
774
  }
765
- setMessages(nextMessages);
766
- }, [runtimeMessages, sessionStreamingText]);
775
+ return nextMessages;
776
+ }, [activeConversationId, runtimeMessages, sessionStreamingText]);
767
777
  useEffect(() => {
768
778
  if (!activeConversationId)
769
779
  return;
@@ -794,7 +804,6 @@ export function useAssistantController({ client, podId, assistantName, assistant
794
804
  setAvailableModels([]);
795
805
  setConversationModelState(null);
796
806
  setConversations([]);
797
- setMessages([]);
798
807
  setLocalError(null);
799
808
  setOlderMessagesCursor(null);
800
809
  setIsLoadingConversations(false);
@@ -810,7 +819,6 @@ export function useAssistantController({ client, podId, assistantName, assistant
810
819
  setActiveConversationId(null);
811
820
  setConversationModelState(null);
812
821
  setConversations([]);
813
- setMessages([]);
814
822
  setLocalError(null);
815
823
  clearRuntimeMessages();
816
824
  setOlderMessagesCursor(null);
@@ -823,7 +831,6 @@ export function useAssistantController({ client, podId, assistantName, assistant
823
831
  clearRuntimeMessages();
824
832
  lastAutoLoadedConversationIdRef.current = null;
825
833
  loadingConversationIdRef.current = null;
826
- setMessages([]);
827
834
  setOlderMessagesCursor(null);
828
835
  return;
829
836
  }
@@ -842,12 +849,12 @@ export function useAssistantController({ client, podId, assistantName, assistant
842
849
  loadingConversationIdRef.current = activeConversationId;
843
850
  const loadConversation = async () => {
844
851
  setOlderMessagesCursor(null);
845
- await loadConversationMessages(activeConversationId);
852
+ await loadConversationMessagesRef.current?.(activeConversationId);
846
853
  if (cancelled)
847
854
  return;
848
855
  lastAutoLoadedConversationIdRef.current = activeConversationId;
849
856
  try {
850
- await sessionResumeIfRunning(activeConversationId);
857
+ await resumeIfRunningRef.current?.(activeConversationId);
851
858
  }
852
859
  catch (error) {
853
860
  if (cancelled)
@@ -863,9 +870,9 @@ export function useAssistantController({ client, podId, assistantName, assistant
863
870
  return () => {
864
871
  cancelled = true;
865
872
  };
866
- }, [activeConversationId, clearRuntimeMessages, enabled, loadConversationMessages, sessionResumeIfRunning]);
873
+ }, [activeConversationId, clearRuntimeMessages, enabled]);
867
874
  const stop = useCallback(() => {
868
- const hadActiveStream = sessionIsStreaming || isStreaming;
875
+ const hadActiveStream = sessionIsStreamingRef.current || isStreamingRef.current;
869
876
  sessionCancel();
870
877
  setIsStreaming(false);
871
878
  const conversationId = activeConversationIdRef.current;
@@ -881,13 +888,34 @@ export function useAssistantController({ client, podId, assistantName, assistant
881
888
  touchConversation(conversationId, { status: previousStatus });
882
889
  setLocalError((prev) => prev || (error instanceof Error ? error.message : "Failed to stop conversation"));
883
890
  });
884
- }, [isStreaming, sessionCancel, sessionIsStreaming, sessionStop, touchConversation]);
891
+ }, [sessionCancel, sessionStop, touchConversation]);
885
892
  const selectConversation = useCallback((conversationId) => {
886
- if (sessionIsStreaming || isStreaming) {
893
+ if (sessionIsStreamingRef.current || isStreamingRef.current) {
887
894
  sessionCancel();
888
895
  setIsStreaming(false);
889
896
  }
890
- const wasSameConversation = conversationId && conversationId === activeConversationIdRef.current;
897
+ const currentConversationId = activeConversationIdRef.current;
898
+ if (conversationId && conversationId === currentConversationId) {
899
+ if (loadingConversationIdRef.current === conversationId
900
+ || lastAutoLoadedConversationIdRef.current === conversationId) {
901
+ return;
902
+ }
903
+ loadingConversationIdRef.current = conversationId;
904
+ setLocalError(null);
905
+ setOlderMessagesCursor(null);
906
+ void loadConversationMessagesRef.current?.(conversationId)
907
+ .then(() => resumeIfRunningRef.current?.(conversationId))
908
+ .catch((error) => {
909
+ setLocalError((prev) => prev || (error instanceof Error ? error.message : "Failed to resume conversation"));
910
+ })
911
+ .finally(() => {
912
+ if (loadingConversationIdRef.current === conversationId) {
913
+ loadingConversationIdRef.current = null;
914
+ }
915
+ lastAutoLoadedConversationIdRef.current = conversationId;
916
+ });
917
+ return;
918
+ }
891
919
  suppressAutoSelectRef.current = conversationId === null;
892
920
  setLocalError(null);
893
921
  activeConversationIdRef.current = conversationId;
@@ -895,15 +923,8 @@ export function useAssistantController({ client, podId, assistantName, assistant
895
923
  loadingConversationIdRef.current = null;
896
924
  setOlderMessagesCursor(null);
897
925
  clearRuntimeMessages();
898
- setMessages([]);
899
- if (wasSameConversation) {
900
- void loadConversationMessages(conversationId);
901
- void sessionResumeIfRunning(conversationId).catch((error) => {
902
- setLocalError((prev) => prev || (error instanceof Error ? error.message : "Failed to resume conversation"));
903
- });
904
- }
905
926
  setActiveConversationId(conversationId);
906
- }, [clearRuntimeMessages, isStreaming, loadConversationMessages, sessionCancel, sessionIsStreaming, sessionResumeIfRunning]);
927
+ }, [clearRuntimeMessages, sessionCancel]);
907
928
  const resetConversationState = useCallback((keepPendingFiles = false) => {
908
929
  stop();
909
930
  clearRuntimeMessages();
@@ -913,7 +934,6 @@ export function useAssistantController({ client, podId, assistantName, assistant
913
934
  loadingConversationIdRef.current = null;
914
935
  skipInitialLoadConversationIdsRef.current.clear();
915
936
  setActiveConversationId(null);
916
- setMessages([]);
917
937
  setLocalError(null);
918
938
  setOlderMessagesCursor(null);
919
939
  if (!keepPendingFiles) {
@@ -945,7 +965,6 @@ export function useAssistantController({ client, podId, assistantName, assistant
945
965
  setActiveConversationId(createdConversation.id);
946
966
  setConversationModelState((createdConversation.model ?? conversationModel ?? null));
947
967
  clearRuntimeMessages();
948
- setMessages([]);
949
968
  setOlderMessagesCursor(null);
950
969
  return createdConversation.id;
951
970
  }, [clearRuntimeMessages, conversationModel, scope, sessionCreateConversation]);
@@ -1099,7 +1118,7 @@ export function useAssistantController({ client, podId, assistantName, assistant
1099
1118
  const activeConversation = conversations.find((conversation) => conversation.id === activeConversationId);
1100
1119
  return isConversationRunning(activeConversation?.status);
1101
1120
  }, [activeConversationId, conversations]);
1102
- return {
1121
+ return useMemo(() => ({
1103
1122
  messages,
1104
1123
  conversations,
1105
1124
  activeConversationId,
@@ -1125,5 +1144,31 @@ export function useAssistantController({ client, podId, assistantName, assistant
1125
1144
  loadOlderMessages,
1126
1145
  clearMessages,
1127
1146
  stop,
1128
- };
1147
+ }), [
1148
+ activeConversationId,
1149
+ availableModels,
1150
+ clearMessages,
1151
+ clearPendingFiles,
1152
+ completedActions,
1153
+ conversationModel,
1154
+ conversations,
1155
+ error,
1156
+ isActiveConversationRunning,
1157
+ isLoading,
1158
+ isLoadingConversations,
1159
+ isLoadingMessages,
1160
+ isLoadingOlderMessages,
1161
+ isUploadingFiles,
1162
+ loadOlderMessages,
1163
+ messages,
1164
+ olderMessagesCursor,
1165
+ pendingActions,
1166
+ pendingFiles,
1167
+ removePendingFile,
1168
+ selectConversation,
1169
+ sendMessage,
1170
+ setConversationModel,
1171
+ stop,
1172
+ uploadFiles,
1173
+ ]);
1129
1174
  }
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  function isRecord(value) {
3
3
  return !!value && typeof value === "object" && !Array.isArray(value);
4
4
  }
@@ -115,6 +115,7 @@ export function useAssistantRuntime({ conversationId = null, sessionConversation
115
115
  setRuntimeMessages([]);
116
116
  }, []);
117
117
  useEffect(() => {
118
+ lastSessionMessageIdRef.current = null;
118
119
  setRuntimeMessages((previous) => {
119
120
  if (!conversationId) {
120
121
  return [];
@@ -122,12 +123,15 @@ export function useAssistantRuntime({ conversationId = null, sessionConversation
122
123
  return previous.filter((message) => message.conversation_id === conversationId);
123
124
  });
124
125
  }, [conversationId]);
126
+ const lastSessionMessageIdRef = useRef(null);
125
127
  useEffect(() => {
126
128
  if (sessionMessages.length === 0)
127
129
  return;
128
- // Session message state can lag one render behind active conversation
129
- // updates. Prefer the session's own conversation id as fallback so we
130
- // never relabel stale messages into the newly selected thread.
130
+ const lastSessionMessage = sessionMessages[sessionMessages.length - 1];
131
+ const lastSessionId = lastSessionMessage?.id ?? null;
132
+ if (lastSessionId && lastSessionId === lastSessionMessageIdRef.current)
133
+ return;
134
+ lastSessionMessageIdRef.current = lastSessionId;
131
135
  const fallbackConversationId = sessionConversationId ?? conversationId;
132
136
  const normalized = sessionMessages
133
137
  .map((message) => toRuntimeMessage(message, fallbackConversationId))
@@ -61,6 +61,8 @@ export function useAssistantSession(options) {
61
61
  const statusRef = useRef(undefined);
62
62
  const streamingTextRef = useRef("");
63
63
  const autoResumedKeyRef = useRef(null);
64
+ const autoLoadInFlightKeyRef = useRef(null);
65
+ const lastAutoLoadedKeyRef = useRef(null);
64
66
  const onEventRef = useRef(onEvent);
65
67
  const onStatusRef = useRef(onStatus);
66
68
  const onMessageRef = useRef(onMessage);
@@ -73,6 +75,8 @@ export function useAssistantSession(options) {
73
75
  return currentConversationId;
74
76
  }
75
77
  autoResumedKeyRef.current = null;
78
+ autoLoadInFlightKeyRef.current = null;
79
+ lastAutoLoadedKeyRef.current = null;
76
80
  streamingTextRef.current = "";
77
81
  setStreamingText("");
78
82
  setConversation(null);
@@ -113,7 +117,12 @@ export function useAssistantSession(options) {
113
117
  onStatusRef.current?.(normalized);
114
118
  }
115
119
  }, []);
120
+ const pendingStreamingFlushRef = useRef(null);
116
121
  const clearStreamingText = useCallback(() => {
122
+ if (pendingStreamingFlushRef.current) {
123
+ clearTimeout(pendingStreamingFlushRef.current);
124
+ pendingStreamingFlushRef.current = null;
125
+ }
117
126
  streamingTextRef.current = "";
118
127
  setStreamingText("");
119
128
  }, []);
@@ -121,7 +130,19 @@ export function useAssistantSession(options) {
121
130
  if (!token)
122
131
  return;
123
132
  streamingTextRef.current += token;
124
- setStreamingText(streamingTextRef.current);
133
+ if (!pendingStreamingFlushRef.current) {
134
+ pendingStreamingFlushRef.current = setTimeout(() => {
135
+ pendingStreamingFlushRef.current = null;
136
+ setStreamingText(streamingTextRef.current);
137
+ }, 0);
138
+ }
139
+ }, []);
140
+ useEffect(() => {
141
+ return () => {
142
+ if (pendingStreamingFlushRef.current) {
143
+ clearTimeout(pendingStreamingFlushRef.current);
144
+ }
145
+ };
125
146
  }, []);
126
147
  const cancel = useCallback(() => {
127
148
  abortRef.current?.abort();
@@ -485,10 +506,18 @@ export function useAssistantSession(options) {
485
506
  }, [status]);
486
507
  useEffect(() => {
487
508
  if (!autoLoad || !conversationId) {
509
+ autoLoadInFlightKeyRef.current = null;
510
+ lastAutoLoadedKeyRef.current = null;
511
+ return;
512
+ }
513
+ const bootstrapKey = `${conversationId}:${autoResume ? "resume" : "load"}`;
514
+ if (autoLoadInFlightKeyRef.current === bootstrapKey
515
+ || lastAutoLoadedKeyRef.current === bootstrapKey) {
488
516
  return;
489
517
  }
490
518
  const controller = new AbortController();
491
519
  let cancelled = false;
520
+ autoLoadInFlightKeyRef.current = bootstrapKey;
492
521
  const bootstrapConversation = async () => {
493
522
  const latestConversation = await refreshConversation(conversationId);
494
523
  if (cancelled)
@@ -503,7 +532,20 @@ export function useAssistantSession(options) {
503
532
  return;
504
533
  await resumeIfRunning(conversationId);
505
534
  };
506
- void bootstrapConversation();
535
+ void bootstrapConversation()
536
+ .catch((bootstrapError) => {
537
+ if (cancelled)
538
+ return;
539
+ const normalized = normalizeError(bootstrapError, "Failed to load assistant conversation.");
540
+ setError(normalized);
541
+ onErrorRef.current?.(bootstrapError);
542
+ })
543
+ .finally(() => {
544
+ if (autoLoadInFlightKeyRef.current === bootstrapKey) {
545
+ autoLoadInFlightKeyRef.current = null;
546
+ }
547
+ lastAutoLoadedKeyRef.current = bootstrapKey;
548
+ });
507
549
  return () => {
508
550
  cancelled = true;
509
551
  controller.abort();
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useMemo, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { extractConversationMessageText, getLatestAssistantMessage, isConversationRunningStatus, normalizeConversationStatus, sortConversationMessagesByCreatedAt, } from "./assistant-output.js";
3
3
  import { useAssistantSession, } from "./useAssistantSession.js";
4
4
  function resolveConversationId(preferred, fallback) {
@@ -16,6 +16,8 @@ export function useConversationMessages({ client, podId, assistantName, assistan
16
16
  const [nextPageToken, setNextPageToken] = useState(null);
17
17
  const [isLoading, setIsLoading] = useState(false);
18
18
  const [isLoadingOlder, setIsLoadingOlder] = useState(false);
19
+ const autoLoadInFlightKeyRef = useRef(null);
20
+ const lastAutoLoadedKeyRef = useRef(null);
19
21
  const { conversation: sessionConversation, conversationId: sessionConversationId, messages: sessionMessages, status, streamingText, isStreaming, error, refreshConversation, loadMessages, sendMessage, resume, resumeIfRunning, stop, cancel, clearMessages: clearSessionMessages, createConversation, } = useAssistantSession({
20
22
  client,
21
23
  podId,
@@ -74,6 +76,8 @@ export function useConversationMessages({ client, podId, assistantName, assistan
74
76
  }, [enabled, isLoading, isLoadingOlder, limit, loadMessages, nextPageToken, sessionConversationId]);
75
77
  useEffect(() => {
76
78
  if (!enabled || !conversationId) {
79
+ autoLoadInFlightKeyRef.current = null;
80
+ lastAutoLoadedKeyRef.current = null;
77
81
  setNextPageToken(null);
78
82
  setIsLoading(false);
79
83
  setIsLoadingOlder(false);
@@ -84,6 +88,12 @@ export function useConversationMessages({ client, podId, assistantName, assistan
84
88
  setNextPageToken(null);
85
89
  if (!autoLoad)
86
90
  return;
91
+ const bootstrapKey = `${conversationId}:${limit}:${autoResume ? "resume" : "load"}`;
92
+ if (autoLoadInFlightKeyRef.current === bootstrapKey
93
+ || lastAutoLoadedKeyRef.current === bootstrapKey) {
94
+ return;
95
+ }
96
+ autoLoadInFlightKeyRef.current = bootstrapKey;
87
97
  let cancelled = false;
88
98
  const bootstrap = async () => {
89
99
  await refresh({ conversationId, limit });
@@ -91,7 +101,14 @@ export function useConversationMessages({ client, podId, assistantName, assistan
91
101
  return;
92
102
  await resumeIfRunning(conversationId);
93
103
  };
94
- void bootstrap();
104
+ void bootstrap()
105
+ .catch(() => undefined)
106
+ .finally(() => {
107
+ if (autoLoadInFlightKeyRef.current === bootstrapKey) {
108
+ autoLoadInFlightKeyRef.current = null;
109
+ }
110
+ lastAutoLoadedKeyRef.current = bootstrapKey;
111
+ });
95
112
  return () => {
96
113
  cancelled = true;
97
114
  };
@@ -0,0 +1,18 @@
1
+ import type { LemmaClient } from "../client.js";
2
+ import type { FileResponse } from "../types.js";
3
+ export interface UseFileOptions {
4
+ client: LemmaClient;
5
+ podId?: string;
6
+ path?: string | null;
7
+ enabled?: boolean;
8
+ autoLoad?: boolean;
9
+ }
10
+ export interface UseFileResult {
11
+ file: FileResponse | null;
12
+ isLoading: boolean;
13
+ error: Error | null;
14
+ refresh: (overrides?: {
15
+ path?: string | null;
16
+ }) => Promise<FileResponse | null>;
17
+ }
18
+ export declare function useFile({ client, podId, path, enabled, autoLoad, }: UseFileOptions): UseFileResult;
@@ -0,0 +1,58 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { normalizeError, resolvePodClient } from "./utils.js";
3
+ export function useFile({ client, podId, path = null, enabled = true, autoLoad = true, }) {
4
+ const [file, setFile] = useState(null);
5
+ const [isLoading, setIsLoading] = useState(false);
6
+ const [error, setError] = useState(null);
7
+ const trimmedPath = typeof path === "string" ? path.trim() : "";
8
+ const isEnabled = enabled && trimmedPath.length > 0;
9
+ const refresh = useCallback(async (overrides = {}, signal) => {
10
+ const nextPath = typeof overrides.path === "string" ? overrides.path.trim() : trimmedPath;
11
+ if (!enabled || nextPath.length === 0) {
12
+ setFile(null);
13
+ setError(null);
14
+ setIsLoading(false);
15
+ return null;
16
+ }
17
+ setIsLoading(true);
18
+ setError(null);
19
+ try {
20
+ const scopedClient = resolvePodClient(client, podId);
21
+ const nextFile = await scopedClient.files.get(nextPath);
22
+ if (signal?.aborted)
23
+ return null;
24
+ setFile(nextFile);
25
+ return nextFile;
26
+ }
27
+ catch (refreshError) {
28
+ if (signal?.aborted)
29
+ return null;
30
+ setError(normalizeError(refreshError, "Failed to load file."));
31
+ setFile(null);
32
+ return null;
33
+ }
34
+ finally {
35
+ if (!signal?.aborted)
36
+ setIsLoading(false);
37
+ }
38
+ }, [client, enabled, podId, trimmedPath]);
39
+ useEffect(() => {
40
+ if (!isEnabled) {
41
+ setFile(null);
42
+ setError(null);
43
+ setIsLoading(false);
44
+ return;
45
+ }
46
+ if (!autoLoad)
47
+ return;
48
+ const controller = new AbortController();
49
+ void refresh({}, controller.signal);
50
+ return () => controller.abort();
51
+ }, [autoLoad, isEnabled, refresh]);
52
+ return useMemo(() => ({
53
+ file,
54
+ isLoading,
55
+ error,
56
+ refresh,
57
+ }), [error, file, isLoading, refresh]);
58
+ }
@@ -0,0 +1,23 @@
1
+ import type { LemmaClient } from "../client.js";
2
+ export type FilePreviewMode = "rendered" | "artifact";
3
+ export interface UseFilePreviewOptions {
4
+ client: LemmaClient;
5
+ podId?: string;
6
+ path?: string | null;
7
+ enabled?: boolean;
8
+ autoLoad?: boolean;
9
+ mode?: FilePreviewMode;
10
+ artifact?: string;
11
+ }
12
+ export interface UseFilePreviewResult {
13
+ content: string | null;
14
+ blob: Blob | null;
15
+ isLoading: boolean;
16
+ error: Error | null;
17
+ refresh: (overrides?: {
18
+ path?: string | null;
19
+ mode?: FilePreviewMode;
20
+ artifact?: string;
21
+ }) => Promise<string | null>;
22
+ }
23
+ export declare function useFilePreview({ client, podId, path, enabled, autoLoad, mode, artifact, }: UseFilePreviewOptions): UseFilePreviewResult;
@@ -0,0 +1,76 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { normalizeError, resolvePodClient } from "./utils.js";
3
+ export function useFilePreview({ client, podId, path = null, enabled = true, autoLoad = true, mode = "rendered", artifact = "document.md", }) {
4
+ const [content, setContent] = useState(null);
5
+ const [blob, setBlob] = useState(null);
6
+ const [isLoading, setIsLoading] = useState(false);
7
+ const [error, setError] = useState(null);
8
+ const trimmedPath = typeof path === "string" ? path.trim() : "";
9
+ const isEnabled = enabled && trimmedPath.length > 0;
10
+ const refresh = useCallback(async (overrides = {}, signal) => {
11
+ const nextPath = typeof overrides.path === "string" ? overrides.path.trim() : trimmedPath;
12
+ const nextMode = overrides.mode ?? mode;
13
+ const nextArtifact = overrides.artifact ?? artifact;
14
+ if (!enabled || nextPath.length === 0) {
15
+ setContent(null);
16
+ setBlob(null);
17
+ setError(null);
18
+ setIsLoading(false);
19
+ return null;
20
+ }
21
+ setIsLoading(true);
22
+ setError(null);
23
+ try {
24
+ const scopedClient = resolvePodClient(client, podId);
25
+ if (nextMode === "artifact") {
26
+ const nextBlob = await scopedClient.files.converted.download(nextPath, nextArtifact);
27
+ const text = await nextBlob.text();
28
+ if (signal?.aborted)
29
+ return null;
30
+ setBlob(nextBlob);
31
+ setContent(text);
32
+ return text;
33
+ }
34
+ const rendered = await scopedClient.files.converted.render(nextPath);
35
+ const text = typeof rendered === "string" ? rendered : String(rendered ?? "");
36
+ if (signal?.aborted)
37
+ return null;
38
+ setBlob(null);
39
+ setContent(text);
40
+ return text;
41
+ }
42
+ catch (refreshError) {
43
+ if (signal?.aborted)
44
+ return null;
45
+ setError(normalizeError(refreshError, "Failed to load file preview."));
46
+ setContent(null);
47
+ setBlob(null);
48
+ return null;
49
+ }
50
+ finally {
51
+ if (!signal?.aborted)
52
+ setIsLoading(false);
53
+ }
54
+ }, [artifact, client, enabled, mode, podId, trimmedPath]);
55
+ useEffect(() => {
56
+ if (!isEnabled) {
57
+ setContent(null);
58
+ setBlob(null);
59
+ setError(null);
60
+ setIsLoading(false);
61
+ return;
62
+ }
63
+ if (!autoLoad)
64
+ return;
65
+ const controller = new AbortController();
66
+ void refresh({}, controller.signal);
67
+ return () => controller.abort();
68
+ }, [autoLoad, isEnabled, refresh]);
69
+ return useMemo(() => ({
70
+ content,
71
+ blob,
72
+ isLoading,
73
+ error,
74
+ refresh,
75
+ }), [blob, content, error, isLoading, refresh]);
76
+ }
@@ -0,0 +1,26 @@
1
+ import type { LemmaClient } from "../client.js";
2
+ import type { FileSearchResponse, FileSearchResultSchema, SearchMethod } from "../types.js";
3
+ export interface UseFileSearchOptions {
4
+ client: LemmaClient;
5
+ podId?: string;
6
+ query?: string;
7
+ enabled?: boolean;
8
+ autoLoad?: boolean;
9
+ minQueryLength?: number;
10
+ limit?: number;
11
+ searchMethod?: SearchMethod;
12
+ }
13
+ export interface UseFileSearchResult {
14
+ response: FileSearchResponse | null;
15
+ results: FileSearchResultSchema[];
16
+ totalResults: number;
17
+ isLoading: boolean;
18
+ error: Error | null;
19
+ search: (overrides?: {
20
+ query?: string;
21
+ limit?: number;
22
+ searchMethod?: SearchMethod;
23
+ }) => Promise<FileSearchResponse | null>;
24
+ reset: () => void;
25
+ }
26
+ export declare function useFileSearch({ client, podId, query, enabled, autoLoad, minQueryLength, limit, searchMethod, }: UseFileSearchOptions): UseFileSearchResult;