chore: sync local workspace changes

This commit is contained in:
2026-05-07 11:03:47 +09:00
parent 2df0ba30cb
commit 82c0d8a197
217 changed files with 44873 additions and 1678 deletions

View File

@@ -47,7 +47,13 @@ export type ChatGateway = {
payload: Partial<
Pick<
ChatConversationSummary,
'title' | 'chatTypeId' | 'lastChatTypeId' | 'contextLabel' | 'contextDescription' | 'notifyOffline'
| 'title'
| 'chatTypeId'
| 'lastChatTypeId'
| 'generalSectionName'
| 'contextLabel'
| 'contextDescription'
| 'notifyOffline'
>
>,
) => Promise<ChatConversationSummary>;

View File

@@ -20,6 +20,7 @@ type PendingChatRequest = {
requestId: string;
text: string;
mode: 'queue' | 'direct';
omitPromptHistory?: boolean;
chatTypeId: string;
chatTypeLabel: string;
chatTypeDescription: string;
@@ -35,6 +36,7 @@ type PendingContextConfirm = {
chatTypeDescription: string;
includedContextCount: number;
omittedContextCount: number;
omitPromptHistory?: boolean;
};
type SelectedChatType = {
@@ -87,6 +89,11 @@ type UseConversationComposerControllerOptions = {
scrollViewportToBottom: () => void;
};
type SendMessageOptions = {
mode: 'queue' | 'direct';
draftText?: string;
};
export function useConversationComposerController({
activeSessionId,
appConfigChat,
@@ -219,13 +226,14 @@ export function useConversationComposerController({
const executeSendMessage = useCallback(
(request: PendingContextConfirm) => {
const { mode, text, chatTypeId, chatTypeLabel, chatTypeDescription } = request;
const { mode, text, chatTypeId, chatTypeLabel, chatTypeDescription, omitPromptHistory } = request;
const requestId = `client-${Date.now().toString(36)}`;
const outgoingRequest: PendingChatRequest = {
sessionId: activeSessionId,
requestId,
text,
mode,
omitPromptHistory: omitPromptHistory === true,
chatTypeId,
chatTypeLabel,
chatTypeDescription,
@@ -361,12 +369,12 @@ export function useConversationComposerController({
);
const sendMessage = useCallback(
(mode: 'queue' | 'direct') => {
({ mode, draftText }: SendMessageOptions) => {
if (isComposerAttachmentUploading) {
return;
}
const trimmed = buildOutgoingMessageText(draft, composerAttachments).trim();
const trimmed = buildOutgoingMessageText(draftText ?? draft, composerAttachments).trim();
if (!trimmed) {
return;
@@ -427,11 +435,11 @@ export function useConversationComposerController({
);
const handleSend = useCallback(() => {
sendMessage('queue');
sendMessage({ mode: 'queue' });
}, [sendMessage]);
const handleSendImmediate = useCallback(() => {
sendMessage('direct');
sendMessage({ mode: 'direct' });
}, [sendMessage]);
return {

View File

@@ -6,6 +6,7 @@ import { chatGateway } from '../data/chatGateway';
type UseConversationListDataOptions = {
requestedSessionId: string;
enabled?: boolean;
};
type UseConversationListDataResult = {
@@ -17,37 +18,71 @@ type UseConversationListDataResult = {
setConversationSearch: Dispatch<SetStateAction<string>>;
};
const CONVERSATION_LIST_POLL_INTERVAL_MS = 5000;
function mergeConversationItemsPreservingRequestedSession(
nextItems: ChatConversationSummary[],
previousItems: ChatConversationSummary[],
requestedSessionId: string,
) {
const previousBySessionId = new Map(previousItems.map((item) => [item.sessionId, item] as const));
const normalizedNextItems = nextItems.map((item) => {
const previousItem = previousBySessionId.get(item.sessionId);
if (!previousItem) {
return item;
}
const chatTypeId = item.chatTypeId?.trim() || previousItem.chatTypeId?.trim() || null;
const lastChatTypeId =
item.lastChatTypeId?.trim() ||
chatTypeId ||
previousItem.lastChatTypeId?.trim() ||
previousItem.chatTypeId?.trim() ||
null;
return {
...item,
chatTypeId,
lastChatTypeId,
generalSectionName: item.generalSectionName?.trim() || previousItem.generalSectionName?.trim() || null,
contextLabel: item.contextLabel?.trim() || previousItem.contextLabel?.trim() || null,
contextDescription: item.contextDescription?.trim() || previousItem.contextDescription?.trim() || null,
lastMessagePreview: item.lastMessagePreview.trim() || previousItem.lastMessagePreview.trim(),
lastResponsePreview: item.lastResponsePreview.trim() || previousItem.lastResponsePreview.trim(),
};
});
const normalizedRequestedSessionId = requestedSessionId.trim();
const nextSessionIds = new Set(normalizedNextItems.map((item) => item.sessionId));
const preservedTransientItems = previousItems.filter((item) => {
if (!item.sessionId || nextSessionIds.has(item.sessionId)) {
return false;
}
return item.isDraftOnly || item.currentJobStatus === 'queued' || item.currentJobStatus === 'started';
});
if (!normalizedRequestedSessionId) {
return sortChatConversationSummaries(nextItems);
return sortChatConversationSummaries([...preservedTransientItems, ...normalizedNextItems]);
}
const hasRequestedSession = nextItems.some((item) => item.sessionId === normalizedRequestedSessionId);
const hasRequestedSession = normalizedNextItems.some((item) => item.sessionId === normalizedRequestedSessionId);
if (hasRequestedSession) {
return sortChatConversationSummaries(nextItems);
return sortChatConversationSummaries([...preservedTransientItems, ...normalizedNextItems]);
}
const preservedRequestedSession =
previousItems.find((item) => item.sessionId === normalizedRequestedSessionId) ?? null;
if (!preservedRequestedSession) {
return sortChatConversationSummaries(nextItems);
return sortChatConversationSummaries([...preservedTransientItems, ...normalizedNextItems]);
}
return sortChatConversationSummaries([preservedRequestedSession, ...nextItems]);
return sortChatConversationSummaries([preservedRequestedSession, ...preservedTransientItems, ...normalizedNextItems]);
}
export function useConversationListData({
requestedSessionId,
enabled = true,
}: UseConversationListDataOptions): UseConversationListDataResult {
const [conversationItems, setConversationItems] = useState<ChatConversationSummary[]>([]);
const [isConversationListLoading, setIsConversationListLoading] = useState(false);
@@ -104,65 +139,17 @@ export function useConversationListData({
useEffect(() => {
isMountedRef.current = true;
setIsConversationListLoading(true);
void loadConversationItems();
if (enabled) {
setIsConversationListLoading(true);
void loadConversationItems();
} else {
setIsConversationListLoading(false);
}
return () => {
isMountedRef.current = false;
};
}, [loadConversationItems]);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
let intervalId: number | null = null;
const startPolling = () => {
if (intervalId != null || document.visibilityState !== 'visible') {
return;
}
intervalId = window.setInterval(() => {
void loadConversationItems({ silent: true });
}, CONVERSATION_LIST_POLL_INTERVAL_MS);
};
const stopPolling = () => {
if (intervalId == null) {
return;
}
window.clearInterval(intervalId);
intervalId = null;
};
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
void loadConversationItems({ silent: true });
startPolling();
return;
}
stopPolling();
};
const handleFocus = () => {
void loadConversationItems({ silent: true });
startPolling();
};
startPolling();
window.addEventListener('focus', handleFocus);
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
stopPolling();
window.removeEventListener('focus', handleFocus);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [loadConversationItems]);
}, [enabled, loadConversationItems]);
return {
conversationItems,

View File

@@ -14,6 +14,7 @@ type PendingChatRequest = {
requestId: string;
text: string;
mode: 'queue' | 'direct';
omitPromptHistory?: boolean;
chatTypeId: string;
chatTypeLabel: string;
chatTypeDescription: string;

View File

@@ -18,14 +18,40 @@ function mergeConversationRequests(
sessionId: string,
) {
const previousSessionItems = previous.filter((item) => item.sessionId === sessionId);
const previousByRequestId = new Map(previousSessionItems.map((item) => [item.requestId, item] as const));
const incomingRequestIds = new Set(incoming.map((item) => item.requestId));
const mergedIncoming = incoming.map((item) => {
const previousItem = previousByRequestId.get(item.requestId);
if (!previousItem) {
return item;
}
const nextUserText = item.userText.trim() || previousItem.userText.trim();
const nextResponseText = item.responseText.trim() || previousItem.responseText.trim();
const nextStatusMessage = item.statusMessage?.trim() || previousItem.statusMessage?.trim() || null;
return {
...item,
statusMessage: nextStatusMessage,
userMessageId: item.userMessageId ?? previousItem.userMessageId,
userText: nextUserText,
responseMessageId: item.responseMessageId ?? previousItem.responseMessageId,
responseText: nextResponseText,
hasResponse: item.hasResponse || previousItem.hasResponse || nextResponseText.length > 0,
answeredAt: item.answeredAt ?? previousItem.answeredAt,
terminalAt: item.terminalAt ?? previousItem.terminalAt,
};
});
const preservedLocalItems = previousSessionItems.filter((item) => !incomingRequestIds.has(item.requestId));
return [...incoming, ...preservedLocalItems].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
return [...mergedIncoming, ...preservedLocalItems].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
}
type UseConversationRoomDataOptions = {
activeSessionId: string;
activeConversationIsDraftOnly?: boolean;
activeConversationHasLocalActivity?: boolean;
oldestLoadedMessageId: number | null;
reloadKey: number;
shouldForceStickToBottomOnNextLoadRef: MutableRefObject<boolean>;
@@ -50,6 +76,8 @@ type UseConversationRoomDataOptions = {
export function useConversationRoomData({
activeSessionId,
activeConversationIsDraftOnly = false,
activeConversationHasLocalActivity = false,
oldestLoadedMessageId,
reloadKey,
shouldForceStickToBottomOnNextLoadRef,
@@ -85,6 +113,18 @@ export function useConversationRoomData({
return;
}
if (activeConversationIsDraftOnly && !activeConversationHasLocalActivity) {
previousSessionIdRef.current = activeSessionId;
setMessages([]);
setRequestItems((previous) => previous.filter((item) => item.sessionId !== activeSessionId));
setConversationLoadingLabel('첫 요청을 보내면 대화가 저장됩니다.');
setIsConversationContentLoading(false);
setIsLoadingOlderMessages(false);
setHasOlderMessages(false);
setOldestLoadedMessageId(null);
return;
}
let isCancelled = false;
const requestedSessionId = activeSessionId;
@@ -195,6 +235,8 @@ export function useConversationRoomData({
};
}, [
activeSessionId,
activeConversationHasLocalActivity,
activeConversationIsDraftOnly,
captureViewportRestoreSnapshot,
messagesRef,
pendingViewportRestoreRef,