Replace AGENT_CHAT_UNKNOWN_THREAD_ID with null for thread state (#19552)
## Summary This PR refactors the AI chat thread state management to use `null` instead of a sentinel string value (`AGENT_CHAT_UNKNOWN_THREAD_ID`) to represent an uninitialized or new chat thread. This improves type safety and makes the code more idiomatic by using `null` to represent the absence of a value. ## Key Changes - **Removed sentinel constant**: Deleted `AGENT_CHAT_UNKNOWN_THREAD_ID` constant and replaced all usages with `null` - **Updated state types**: Changed `currentAIChatThreadState`, `agentChatLastDiffSyncedThreadState`, and `agentChatDisplayedThreadState` to use `string | null` type with `null` as default value - **Updated component family states**: Modified message-related state families to accept `threadId: string | null` instead of `threadId: string` - **Refined null checks**: Added explicit `null` checks in: - `AgentChatMessagesFetchEffect`: Updated `isNewThread` logic to check for `null` first - `useAIChatThreadClick` and `useSwitchToNewAIChat`: Added guards to only save drafts when `currentAIChatThread !== null` - `AgentChatThreadInitializationEffect`: Added null check before UUID validation - `useEnsureAgentChatThreadIdForSend`: Added null check before comparing with draft key - **Updated fallback logic**: Used nullish coalescing operator (`??`) in `AIChatTab` and `useAIChatEditor` to default to `AGENT_CHAT_NEW_THREAD_DRAFT_KEY` when thread is null - **Enhanced refetch safety**: Added early return in `handleRefetchMessages` to prevent refetching when in new thread state ## Implementation Details - The change maintains backward compatibility by treating `null` the same way the code previously treated `AGENT_CHAT_UNKNOWN_THREAD_ID` - All draft saving operations now safely check for null before attempting to store drafts - The nullish coalescing pattern (`currentAIChatThread ?? AGENT_CHAT_NEW_THREAD_DRAFT_KEY`) ensures proper fallback behavior when accessing draft storage https://claude.ai/code/session_01Pz8KCygSNgBPYsndbMq8f7 --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ export const AIChatTab = () => {
|
||||
const threadIdCreatedFromDraft = useAtomStateValue(
|
||||
threadIdCreatedFromDraftState,
|
||||
);
|
||||
const draftKey = currentAIChatThread;
|
||||
const draftKey = currentAIChatThread ?? AGENT_CHAT_NEW_THREAD_DRAFT_KEY;
|
||||
const editorSectionKey =
|
||||
draftKey !== AGENT_CHAT_NEW_THREAD_DRAFT_KEY &&
|
||||
draftKey === threadIdCreatedFromDraft
|
||||
|
||||
@@ -4,7 +4,6 @@ import { type AgentChatSubscriptionEvent } from 'twenty-shared/ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { AGENT_CHAT_REFETCH_MESSAGES_EVENT_NAME } from '@/ai/constants/AgentChatRefetchMessagesEventName';
|
||||
import { AGENT_CHAT_UNKNOWN_THREAD_ID } from '@/ai/constants/AgentChatUnknownThreadId';
|
||||
import { agentChatFirstLiveSeqState } from '@/ai/states/agentChatFirstLiveSeqState';
|
||||
import { agentChatHandleEventCallbackState } from '@/ai/states/agentChatHandleEventCallbackState';
|
||||
import { AGENT_CHAT_NEW_THREAD_DRAFT_KEY } from '@/ai/states/agentChatDraftsByThreadIdState';
|
||||
@@ -30,8 +29,8 @@ export const AgentChatMessagesFetchEffect = () => {
|
||||
|
||||
const isNewThread = useMemo(
|
||||
() =>
|
||||
currentAIChatThread === AGENT_CHAT_NEW_THREAD_DRAFT_KEY ||
|
||||
currentAIChatThread === AGENT_CHAT_UNKNOWN_THREAD_ID,
|
||||
currentAIChatThread === null ||
|
||||
currentAIChatThread === AGENT_CHAT_NEW_THREAD_DRAFT_KEY,
|
||||
[currentAIChatThread],
|
||||
);
|
||||
|
||||
@@ -111,7 +110,7 @@ export const AgentChatMessagesFetchEffect = () => {
|
||||
const { refetch: refetchAgentChatMessages } = useQueryWithCallbacks(
|
||||
GetChatMessagesDocument,
|
||||
{
|
||||
variables: { threadId: currentAIChatThread },
|
||||
variables: { threadId: currentAIChatThread ?? '' },
|
||||
skip: !isDefined(currentAIChatThread) || isNewThread,
|
||||
onFirstLoad: handleFirstLoad,
|
||||
onDataLoaded: handleDataLoaded,
|
||||
@@ -120,8 +119,12 @@ export const AgentChatMessagesFetchEffect = () => {
|
||||
);
|
||||
|
||||
const handleRefetchMessages = useCallback(() => {
|
||||
if (isNewThread) {
|
||||
return;
|
||||
}
|
||||
|
||||
refetchAgentChatMessages();
|
||||
}, [refetchAgentChatMessages]);
|
||||
}, [refetchAgentChatMessages, isNewThread]);
|
||||
|
||||
useListenToBrowserEvent({
|
||||
eventName: AGENT_CHAT_REFETCH_MESSAGES_EVENT_NAME,
|
||||
|
||||
+4
-1
@@ -88,7 +88,10 @@ export const AgentChatThreadInitializationEffect = () => {
|
||||
}, [storeEntry.status, hasAiSettingsPermission, setAgentChatThreadsLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInitializedAgentChatThreads || isValidUuid(currentAIChatThread)) {
|
||||
if (
|
||||
hasInitializedAgentChatThreads ||
|
||||
(currentAIChatThread !== null && isValidUuid(currentAIChatThread))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const AGENT_CHAT_UNKNOWN_THREAD_ID = 'unknown-thread';
|
||||
@@ -49,7 +49,7 @@ export const useAIChatEditor = () => {
|
||||
const { removeFocusItemFromFocusStackById } =
|
||||
useRemoveFocusItemFromFocusStackById();
|
||||
|
||||
const draftKey = currentAIChatThread;
|
||||
const draftKey = currentAIChatThread ?? AGENT_CHAT_NEW_THREAD_DRAFT_KEY;
|
||||
const initialDraft = agentChatDraftsByThreadId[draftKey] ?? '';
|
||||
const initialContent = textToTiptapContent(initialDraft);
|
||||
|
||||
|
||||
@@ -38,13 +38,14 @@ export const useAIChatThreadClick = (
|
||||
|
||||
const handleThreadClick = (thread: AgentChatThread) => {
|
||||
setThreadIdCreatedFromDraft(null);
|
||||
const previousDraftKey = currentAIChatThread;
|
||||
const isSameThread = thread.id === currentAIChatThread;
|
||||
|
||||
setAgentChatDraftsByThreadId((prev) => ({
|
||||
...prev,
|
||||
[previousDraftKey]: store.get(agentChatInputState.atom),
|
||||
}));
|
||||
if (currentAIChatThread !== null) {
|
||||
setAgentChatDraftsByThreadId((prev) => ({
|
||||
...prev,
|
||||
[currentAIChatThread]: store.get(agentChatInputState.atom),
|
||||
}));
|
||||
}
|
||||
setCurrentAIChatThread(thread.id);
|
||||
|
||||
if (!isSameThread) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { readUIMessageStream, type UIMessageChunk } from 'ai';
|
||||
import { print, type ExecutionResult } from 'graphql';
|
||||
@@ -18,8 +18,6 @@ import { agentChatFirstLiveSeqState } from '@/ai/states/agentChatFirstLiveSeqSta
|
||||
import { agentChatHandleEventCallbackState } from '@/ai/states/agentChatHandleEventCallbackState';
|
||||
import { agentChatIsStreamingState } from '@/ai/states/agentChatIsStreamingState';
|
||||
import { agentChatMessagesComponentFamilyState } from '@/ai/states/agentChatMessagesComponentFamilyState';
|
||||
import { agentChatStreamWriterState } from '@/ai/states/agentChatStreamWriterState';
|
||||
import { agentChatSubscriptionDisposeState } from '@/ai/states/agentChatSubscriptionDisposeState';
|
||||
import { agentChatUsageState } from '@/ai/states/agentChatUsageState';
|
||||
import { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTitleState';
|
||||
import { dispatchBrowserEvent } from '@/browser-event/utils/dispatchBrowserEvent';
|
||||
@@ -98,47 +96,38 @@ export const useAgentChatSubscription = (threadId: string | null) => {
|
||||
const store = useStore();
|
||||
const sseClient = useAtomStateValue(sseClientState);
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
const writer = store.get(agentChatStreamWriterState.atom);
|
||||
|
||||
if (isDefined(writer)) {
|
||||
writer.close().catch(() => {});
|
||||
store.set(agentChatStreamWriterState.atom, null);
|
||||
}
|
||||
|
||||
const dispose = store.get(agentChatSubscriptionDisposeState.atom);
|
||||
|
||||
if (isDefined(dispose)) {
|
||||
dispose();
|
||||
store.set(agentChatSubscriptionDisposeState.atom, null);
|
||||
}
|
||||
|
||||
if (store.get(agentChatIsStreamingState.atom)) {
|
||||
store.set(agentChatIsStreamingState.atom, false);
|
||||
}
|
||||
}, [store]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(threadId)) {
|
||||
cleanup();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(sseClient)) {
|
||||
if (!isDefined(threadId) || !isDefined(sseClient)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let bridge: TransformStream<UIMessageChunk> | null = null;
|
||||
let throttleTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let latestMessage: ExtendedUIMessage | null = null;
|
||||
let writer: WritableStreamDefaultWriter<UIMessageChunk> | null = null;
|
||||
let disposed = false;
|
||||
|
||||
store.set(agentChatFirstLiveSeqState.atom, null);
|
||||
|
||||
const closeWriter = () => {
|
||||
if (isDefined(writer)) {
|
||||
writer.close().catch(() => {});
|
||||
writer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupStream = () => {
|
||||
closeWriter();
|
||||
|
||||
if (store.get(agentChatIsStreamingState.atom)) {
|
||||
store.set(agentChatIsStreamingState.atom, false);
|
||||
}
|
||||
};
|
||||
|
||||
const flushToAtom = () => {
|
||||
const messageToFlush = latestMessage;
|
||||
|
||||
if (!isDefined(messageToFlush) || !isDefined(threadId)) {
|
||||
if (!isDefined(messageToFlush)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -244,7 +233,9 @@ export const useAgentChatSubscription = (threadId: string | null) => {
|
||||
}
|
||||
flushToAtom();
|
||||
|
||||
store.set(agentChatIsStreamingState.atom, false);
|
||||
if (!disposed) {
|
||||
store.set(agentChatIsStreamingState.atom, false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEvent = (event: AgentChatSubscriptionEvent) => {
|
||||
@@ -261,22 +252,19 @@ export const useAgentChatSubscription = (threadId: string | null) => {
|
||||
store.set(agentChatIsStreamingState.atom, true);
|
||||
|
||||
bridge = new TransformStream<UIMessageChunk>();
|
||||
store.set(
|
||||
agentChatStreamWriterState.atom,
|
||||
bridge.writable.getWriter(),
|
||||
);
|
||||
writer = bridge.writable.getWriter();
|
||||
|
||||
const adaptedReadable = bridge.readable.pipeThrough(
|
||||
createMidStreamAdapter(),
|
||||
);
|
||||
|
||||
startReadLoop(adaptedReadable).catch(() => {
|
||||
store.set(agentChatIsStreamingState.atom, false);
|
||||
if (!disposed) {
|
||||
store.set(agentChatIsStreamingState.atom, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const writer = store.get(agentChatStreamWriterState.atom);
|
||||
|
||||
if (isDefined(writer)) {
|
||||
writer.write(event.chunk as UIMessageChunk).catch(() => {});
|
||||
}
|
||||
@@ -284,13 +272,7 @@ export const useAgentChatSubscription = (threadId: string | null) => {
|
||||
}
|
||||
|
||||
case 'message-persisted': {
|
||||
const writer = store.get(agentChatStreamWriterState.atom);
|
||||
|
||||
if (isDefined(writer)) {
|
||||
writer.close().catch(() => {});
|
||||
store.set(agentChatStreamWriterState.atom, null);
|
||||
}
|
||||
|
||||
closeWriter();
|
||||
dispatchBrowserEvent(AGENT_CHAT_REFETCH_MESSAGES_EVENT_NAME);
|
||||
break;
|
||||
}
|
||||
@@ -308,13 +290,7 @@ export const useAgentChatSubscription = (threadId: string | null) => {
|
||||
streamError.code = event.code;
|
||||
store.set(agentChatErrorState.atom, streamError);
|
||||
|
||||
const writer = store.get(agentChatStreamWriterState.atom);
|
||||
|
||||
if (isDefined(writer)) {
|
||||
writer.close().catch(() => {});
|
||||
store.set(agentChatStreamWriterState.atom, null);
|
||||
}
|
||||
|
||||
closeWriter();
|
||||
store.set(agentChatIsStreamingState.atom, false);
|
||||
break;
|
||||
}
|
||||
@@ -340,19 +316,21 @@ export const useAgentChatSubscription = (threadId: string | null) => {
|
||||
// graphql-sse handles reconnection automatically
|
||||
},
|
||||
complete: () => {
|
||||
cleanup();
|
||||
if (!disposed) {
|
||||
cleanupStream();
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
store.set(agentChatSubscriptionDisposeState.atom, () => dispose);
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
store.set(agentChatHandleEventCallbackState.atom, null);
|
||||
if (isDefined(throttleTimer)) {
|
||||
clearTimeout(throttleTimer);
|
||||
}
|
||||
cleanup();
|
||||
cleanupStream();
|
||||
dispose();
|
||||
};
|
||||
}, [threadId, sseClient, store, cleanup]);
|
||||
}, [threadId, sseClient, store]);
|
||||
};
|
||||
|
||||
@@ -20,7 +20,10 @@ export const useEnsureAgentChatThreadIdForSend = (
|
||||
> => {
|
||||
const currentThreadId = store.get(currentAIChatThreadState.atom);
|
||||
|
||||
if (currentThreadId !== AGENT_CHAT_NEW_THREAD_DRAFT_KEY) {
|
||||
if (
|
||||
currentThreadId !== null &&
|
||||
currentThreadId !== AGENT_CHAT_NEW_THREAD_DRAFT_KEY
|
||||
) {
|
||||
return currentThreadId;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,15 +35,16 @@ export const useSwitchToNewAIChat = () => {
|
||||
|
||||
const switchToNewChat = () => {
|
||||
setThreadIdCreatedFromDraft(null);
|
||||
const previousDraftKey = currentAIChatThread;
|
||||
const newChatDraft =
|
||||
store.get(agentChatDraftsByThreadIdState.atom)[
|
||||
AGENT_CHAT_NEW_THREAD_DRAFT_KEY
|
||||
] ?? '';
|
||||
setAgentChatDraftsByThreadId((prev) => ({
|
||||
...prev,
|
||||
[previousDraftKey]: store.get(agentChatInputState.atom),
|
||||
}));
|
||||
if (currentAIChatThread !== null) {
|
||||
setAgentChatDraftsByThreadId((prev) => ({
|
||||
...prev,
|
||||
[currentAIChatThread]: store.get(agentChatInputState.atom),
|
||||
}));
|
||||
}
|
||||
store.set(hasTriggeredCreateForDraftState.atom, false);
|
||||
setCurrentAIChatThread(AGENT_CHAT_NEW_THREAD_DRAFT_KEY);
|
||||
setAgentChatInput(newChatDraft);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const agentChatDisplayedThreadState = createAtomState<string>({
|
||||
export const agentChatDisplayedThreadState = createAtomState<string | null>({
|
||||
key: 'ai/agentChatDisplayedThreadState',
|
||||
defaultValue: '',
|
||||
defaultValue: null,
|
||||
});
|
||||
|
||||
+4
-1
@@ -3,7 +3,10 @@ import { createAtomComponentFamilyState } from '@/ui/utilities/state/jotai/utils
|
||||
import { type ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
|
||||
export const agentChatFetchedMessagesComponentFamilyState =
|
||||
createAtomComponentFamilyState<ExtendedUIMessage[], { threadId: string }>({
|
||||
createAtomComponentFamilyState<
|
||||
ExtendedUIMessage[],
|
||||
{ threadId: string | null }
|
||||
>({
|
||||
key: 'agentChatFetchedMessagesComponentFamilyState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: AgentChatComponentInstanceContext,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const agentChatLastDiffSyncedThreadState = createAtomState<string>({
|
||||
export const agentChatLastDiffSyncedThreadState = createAtomState<
|
||||
string | null
|
||||
>({
|
||||
key: 'ai/agentChatLastDiffSyncedThreadState',
|
||||
defaultValue: '',
|
||||
defaultValue: null,
|
||||
});
|
||||
|
||||
+4
-1
@@ -3,7 +3,10 @@ import { createAtomComponentFamilyState } from '@/ui/utilities/state/jotai/utils
|
||||
import { type ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
|
||||
export const agentChatMessagesComponentFamilyState =
|
||||
createAtomComponentFamilyState<ExtendedUIMessage[], { threadId: string }>({
|
||||
createAtomComponentFamilyState<
|
||||
ExtendedUIMessage[],
|
||||
{ threadId: string | null }
|
||||
>({
|
||||
key: 'agentChatMessagesComponentFamilyState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: AgentChatComponentInstanceContext,
|
||||
|
||||
+4
-1
@@ -3,7 +3,10 @@ import { createAtomComponentFamilyState } from '@/ui/utilities/state/jotai/utils
|
||||
import { type ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
|
||||
export const agentChatQueuedMessagesComponentFamilyState =
|
||||
createAtomComponentFamilyState<ExtendedUIMessage[], { threadId: string }>({
|
||||
createAtomComponentFamilyState<
|
||||
ExtendedUIMessage[],
|
||||
{ threadId: string | null }
|
||||
>({
|
||||
key: 'agentChatQueuedMessagesComponentFamilyState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: AgentChatComponentInstanceContext,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { type UIMessageChunk } from 'ai';
|
||||
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const agentChatStreamWriterState =
|
||||
createAtomState<WritableStreamDefaultWriter<UIMessageChunk> | null>({
|
||||
key: 'agentChatStreamWriterState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const agentChatSubscriptionDisposeState = createAtomState<
|
||||
(() => void) | null
|
||||
>({
|
||||
key: 'agentChatSubscriptionDisposeState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import { AGENT_CHAT_UNKNOWN_THREAD_ID } from '@/ai/constants/AgentChatUnknownThreadId';
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const currentAIChatThreadState = createAtomState<string>({
|
||||
export const currentAIChatThreadState = createAtomState<string | null>({
|
||||
key: 'ai/currentAIChatThreadState',
|
||||
defaultValue: AGENT_CHAT_UNKNOWN_THREAD_ID,
|
||||
defaultValue: null,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user