chore: exclude local resource artifacts from main sync
This commit is contained in:
@@ -2,7 +2,9 @@ import type { Dispatch, SetStateAction } from 'react';
|
||||
import { appendClientIdHeader, getOrCreateClientId } from '../clientIdentity';
|
||||
import { getRegisteredAccessToken, hasRegisteredAccessTokenAccess } from '../tokenAccess';
|
||||
import { reportClientError } from '../errorLogApi';
|
||||
import { notifyNotificationMessagesUpdated } from '../notificationApi';
|
||||
import { copyTextToClipboard } from '../../../utils/clipboard';
|
||||
import { resolveConversationUnreadMergeState } from './conversationUnread';
|
||||
import type {
|
||||
ChatActivityEvent,
|
||||
ChatConversationActivityLog,
|
||||
@@ -10,6 +12,8 @@ import type {
|
||||
ChatComposerAttachment,
|
||||
ChatConversationRequest,
|
||||
ChatConversationSummary,
|
||||
ChatSourceChangeSnapshot,
|
||||
ChatSourceChangeSnapshotListResponse,
|
||||
ChatJobEvent,
|
||||
ChatMessage,
|
||||
ChatRuntimeJobDetail,
|
||||
@@ -62,8 +66,87 @@ function getConversationLastMessageSortTime(item: ChatConversationSummary) {
|
||||
);
|
||||
}
|
||||
|
||||
function pickPreferredConversationSummary(
|
||||
left: ChatConversationSummary,
|
||||
right: ChatConversationSummary,
|
||||
) {
|
||||
const leftTime = getConversationLastMessageSortTime(left);
|
||||
const rightTime = getConversationLastMessageSortTime(right);
|
||||
|
||||
if (rightTime !== leftTime) {
|
||||
return rightTime > leftTime ? right : left;
|
||||
}
|
||||
|
||||
const leftUpdatedAt = toConversationSortTime(left.updatedAt);
|
||||
const rightUpdatedAt = toConversationSortTime(right.updatedAt);
|
||||
|
||||
if (rightUpdatedAt !== leftUpdatedAt) {
|
||||
return rightUpdatedAt > leftUpdatedAt ? right : left;
|
||||
}
|
||||
|
||||
return right;
|
||||
}
|
||||
|
||||
function mergeConversationSummaries(
|
||||
existing: ChatConversationSummary,
|
||||
incoming: ChatConversationSummary,
|
||||
) {
|
||||
const preferred = pickPreferredConversationSummary(existing, incoming);
|
||||
const fallback = preferred === existing ? incoming : existing;
|
||||
|
||||
return {
|
||||
...fallback,
|
||||
...preferred,
|
||||
clientId: preferred.clientId ?? fallback.clientId,
|
||||
isDraftOnly: preferred.isDraftOnly ?? fallback.isDraftOnly,
|
||||
title: preferred.title.trim() || fallback.title.trim(),
|
||||
requestBadgeLabel: preferred.requestBadgeLabel?.trim() || fallback.requestBadgeLabel?.trim() || null,
|
||||
chatTypeId: preferred.chatTypeId?.trim() || fallback.chatTypeId?.trim() || null,
|
||||
lastChatTypeId: preferred.lastChatTypeId?.trim() || fallback.lastChatTypeId?.trim() || null,
|
||||
generalSectionName: preferred.generalSectionName?.trim() || fallback.generalSectionName?.trim() || null,
|
||||
contextLabel: preferred.contextLabel?.trim() || fallback.contextLabel?.trim() || null,
|
||||
contextDescription: preferred.contextDescription?.trim() || fallback.contextDescription?.trim() || null,
|
||||
notifyOffline: preferred.notifyOffline ?? fallback.notifyOffline,
|
||||
hasUnreadResponse: resolveConversationUnreadMergeState(existing, incoming),
|
||||
currentRequestId: preferred.currentRequestId?.trim() || fallback.currentRequestId?.trim() || null,
|
||||
currentJobStatus: preferred.currentJobStatus ?? fallback.currentJobStatus,
|
||||
currentJobMessage: preferred.currentJobMessage?.trim() || fallback.currentJobMessage?.trim() || null,
|
||||
currentQueueSize: Math.max(preferred.currentQueueSize ?? 0, fallback.currentQueueSize ?? 0),
|
||||
currentStatusUpdatedAt:
|
||||
preferred.currentStatusUpdatedAt?.trim() || fallback.currentStatusUpdatedAt?.trim() || null,
|
||||
isPendingWork: preferred.isPendingWork ?? fallback.isPendingWork,
|
||||
pendingWorkReason: preferred.pendingWorkReason ?? fallback.pendingWorkReason,
|
||||
lastRequestPreview: preferred.lastRequestPreview.trim() || fallback.lastRequestPreview.trim(),
|
||||
lastMessagePreview: preferred.lastMessagePreview.trim() || fallback.lastMessagePreview.trim(),
|
||||
lastResponsePreview: preferred.lastResponsePreview.trim() || fallback.lastResponsePreview.trim(),
|
||||
createdAt: preferred.createdAt.trim() || fallback.createdAt.trim(),
|
||||
updatedAt: preferred.updatedAt.trim() || fallback.updatedAt.trim(),
|
||||
lastMessageAt: preferred.lastMessageAt?.trim() || fallback.lastMessageAt?.trim() || null,
|
||||
};
|
||||
}
|
||||
|
||||
export function sortChatConversationSummaries(items: ChatConversationSummary[]) {
|
||||
return [...items].sort((left, right) => {
|
||||
const dedupedItems = items.reduce<ChatConversationSummary[]>((result, item) => {
|
||||
const sessionId = item.sessionId.trim();
|
||||
|
||||
if (!sessionId) {
|
||||
result.push(item);
|
||||
return result;
|
||||
}
|
||||
|
||||
const existingIndex = result.findIndex((candidate) => candidate.sessionId.trim() === sessionId);
|
||||
|
||||
if (existingIndex < 0) {
|
||||
result.push(item);
|
||||
return result;
|
||||
}
|
||||
|
||||
const nextItems = [...result];
|
||||
nextItems[existingIndex] = mergeConversationSummaries(nextItems[existingIndex] as ChatConversationSummary, item);
|
||||
return nextItems;
|
||||
}, []);
|
||||
|
||||
return dedupedItems.sort((left, right) => {
|
||||
const leftTime = getConversationLastMessageSortTime(left);
|
||||
const rightTime = getConversationLastMessageSortTime(right);
|
||||
|
||||
@@ -75,6 +158,44 @@ export function sortChatConversationSummaries(items: ChatConversationSummary[])
|
||||
});
|
||||
}
|
||||
|
||||
export function getDefaultRequestStatusMessage(status: ChatConversationRequest['status']) {
|
||||
switch (status) {
|
||||
case 'accepted':
|
||||
return '요청을 접수했습니다.';
|
||||
case 'queued':
|
||||
return '대기열 등록';
|
||||
case 'started':
|
||||
return '요청 처리 중';
|
||||
case 'completed':
|
||||
return '요청 처리 완료';
|
||||
case 'failed':
|
||||
return '요청 처리 실패';
|
||||
case 'cancelled':
|
||||
return '요청 실행 중단';
|
||||
case 'removed':
|
||||
return '요청 기록이 제거되었습니다.';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeConversationRequestStatusMessage(
|
||||
previousItem: Pick<ChatConversationRequest, 'status' | 'statusMessage'> | null | undefined,
|
||||
nextItem: Pick<ChatConversationRequest, 'status' | 'statusMessage'>,
|
||||
) {
|
||||
const nextStatusMessage = nextItem.statusMessage?.trim() || '';
|
||||
|
||||
if (nextStatusMessage) {
|
||||
return nextStatusMessage;
|
||||
}
|
||||
|
||||
if (!previousItem || previousItem.status !== nextItem.status) {
|
||||
return getDefaultRequestStatusMessage(nextItem.status);
|
||||
}
|
||||
|
||||
return previousItem.statusMessage?.trim() || getDefaultRequestStatusMessage(nextItem.status);
|
||||
}
|
||||
|
||||
export const CHAT_CONNECTION = {
|
||||
reconnectDelayMs: 1500,
|
||||
connectTimeoutMs: CONNECT_TIMEOUT_MS,
|
||||
@@ -570,6 +691,22 @@ function mergeActivityLines(existingLines: string[], incomingLines: string[]) {
|
||||
return merged;
|
||||
}
|
||||
|
||||
function mergeActivityLineAtPosition(existingLines: string[], incomingLine: string, lineNo?: number) {
|
||||
const normalizedLine = incomingLine.trim();
|
||||
|
||||
if (!normalizedLine) {
|
||||
return existingLines;
|
||||
}
|
||||
|
||||
if (!Number.isInteger(lineNo) || Number(lineNo) <= 0) {
|
||||
return mergeActivityLines(existingLines, [normalizedLine]);
|
||||
}
|
||||
|
||||
const nextLines = [...existingLines];
|
||||
nextLines[Number(lineNo) - 1] = normalizedLine;
|
||||
return nextLines.filter(Boolean);
|
||||
}
|
||||
|
||||
function buildActivityMessageIndex(messages: ChatMessage[]) {
|
||||
const indexByRequestId = new Map<string, number>();
|
||||
|
||||
@@ -675,7 +812,7 @@ export function appendActivityEventToMessages(previous: ChatMessage[], event: Ch
|
||||
const activityMessageIndex = buildActivityMessageIndex(previous);
|
||||
const existingIndex = activityMessageIndex.get(requestId);
|
||||
const existingMessage = existingIndex == null ? undefined : previous[existingIndex];
|
||||
const mergedLines = mergeActivityLines(getActivityLogLines(existingMessage), [event.line]);
|
||||
const mergedLines = mergeActivityLineAtPosition(getActivityLogLines(existingMessage), event.line, event.lineNo);
|
||||
const nextMessage = createActivityLogPlaceholder(requestId, mergedLines);
|
||||
|
||||
if (!nextMessage) {
|
||||
@@ -1177,6 +1314,13 @@ export async function fetchChatRuntimeSnapshot() {
|
||||
return response.item;
|
||||
}
|
||||
|
||||
export async function fetchChatSourceChanges(limit = 300) {
|
||||
const query = new URLSearchParams();
|
||||
query.set('limit', String(Math.max(1, Math.min(500, Math.round(limit)))));
|
||||
const response = await requestChatApi<ChatSourceChangeSnapshotListResponse>(`/source-changes?${query.toString()}`);
|
||||
return response.items.map((item) => normalizeChatSourceChangeSnapshot(item));
|
||||
}
|
||||
|
||||
export async function fetchChatRuntimeJobDetail(requestId: string) {
|
||||
const response = await requestChatApi<{ ok: boolean; item: ChatRuntimeJobDetail }>(
|
||||
`/runtime/jobs/${encodeURIComponent(requestId)}`,
|
||||
@@ -1217,6 +1361,32 @@ export async function rollbackChatRuntimeJob(requestId: string, sessionId?: stri
|
||||
return response.rolledBack;
|
||||
}
|
||||
|
||||
function normalizeChatSourceChangeSnapshot(item: ChatSourceChangeSnapshot): ChatSourceChangeSnapshot {
|
||||
return {
|
||||
...item,
|
||||
clientId: item.clientId?.trim() || null,
|
||||
conversationTitle: item.conversationTitle.trim() || '새 대화',
|
||||
chatTypeId: item.chatTypeId?.trim() || null,
|
||||
chatTypeLabel: item.chatTypeLabel.trim(),
|
||||
requestId: item.requestId.trim(),
|
||||
requestTitle: item.requestTitle.trim() || item.requestId.trim(),
|
||||
questionText: item.questionText,
|
||||
answerText: item.answerText,
|
||||
status: item.status,
|
||||
sourceChangedAt: item.sourceChangedAt,
|
||||
updatedAt: item.updatedAt,
|
||||
featureTags: item.featureTags.map((value) => value.trim()).filter(Boolean),
|
||||
changedFiles: item.changedFiles.map((value) => value.trim()).filter(Boolean),
|
||||
currentSourceFiles: item.currentSourceFiles.map((value) => value.trim()).filter(Boolean),
|
||||
diffBlocks: item.diffBlocks.map((value) => value.trim()).filter(Boolean),
|
||||
hasSourceChanges: item.hasSourceChanges === true,
|
||||
reviewStatus: item.reviewStatus === 'reviewed' ? 'reviewed' : 'not-reviewed',
|
||||
sourceChangeKind: item.sourceChangeKind === 'verification-group' ? 'verification-group' : 'request',
|
||||
sourceEntryIds: (Array.isArray(item.sourceEntryIds) ? item.sourceEntryIds : []).map((value) => String(value).trim()).filter(Boolean),
|
||||
conversationDeletedAt: item.conversationDeletedAt?.trim() || null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function uploadChatComposerFile(sessionId: string, file: File) {
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const resolvedMimeType = resolveUploadMimeType(file);
|
||||
@@ -1293,6 +1463,7 @@ export async function uploadChatComposerFile(sessionId: string, file: File) {
|
||||
export async function createChatConversationRoom(args: {
|
||||
sessionId: string;
|
||||
title?: string;
|
||||
requestBadgeLabel?: string | null;
|
||||
chatTypeId?: string | null;
|
||||
lastChatTypeId?: string | null;
|
||||
generalSectionName?: string | null;
|
||||
@@ -1307,6 +1478,7 @@ export async function createChatConversationRoom(args: {
|
||||
body: JSON.stringify({
|
||||
sessionId: args.sessionId,
|
||||
title: args.title ?? '새 대화',
|
||||
requestBadgeLabel: args.requestBadgeLabel ?? null,
|
||||
chatTypeId: args.chatTypeId ?? null,
|
||||
lastChatTypeId: args.lastChatTypeId ?? args.chatTypeId ?? null,
|
||||
generalSectionName: args.generalSectionName ?? null,
|
||||
@@ -1345,6 +1517,7 @@ export async function updateChatConversationRoom(
|
||||
sessionId: string,
|
||||
payload: {
|
||||
title?: string;
|
||||
requestBadgeLabel?: string | null;
|
||||
chatTypeId?: string | null;
|
||||
lastChatTypeId?: string | null;
|
||||
generalSectionName?: string | null;
|
||||
@@ -1464,8 +1637,9 @@ export function upsertChatMessage(previous: ChatMessage[], incoming: ChatMessage
|
||||
Boolean(
|
||||
incoming.clientRequestId &&
|
||||
message.clientRequestId &&
|
||||
incoming.author === 'user' &&
|
||||
message.author === 'user' &&
|
||||
(incoming.author === 'user' || incoming.author === 'codex') &&
|
||||
(message.author === 'user' || message.author === 'codex') &&
|
||||
incoming.author === message.author &&
|
||||
incoming.clientRequestId === message.clientRequestId,
|
||||
),
|
||||
);
|
||||
@@ -1800,7 +1974,27 @@ export function mergeRecoveredChatMessages(previous: ChatMessage[], incoming: Ch
|
||||
};
|
||||
});
|
||||
|
||||
const unmatchedLocalMessages = Array.from(previousBuckets.values()).flat();
|
||||
const incomingUserRequestIds = new Set(
|
||||
incoming
|
||||
.filter((message) => message.author === 'user')
|
||||
.map((message) => getChatMessageRequestId(message))
|
||||
.filter(Boolean),
|
||||
);
|
||||
const unmatchedLocalMessages = Array.from(previousBuckets.values())
|
||||
.flat()
|
||||
.filter((message) => {
|
||||
if (!isMissingRequestMessage(message)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const requestId = getChatMessageRequestId(message);
|
||||
|
||||
if (!requestId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !incomingUserRequestIds.has(requestId);
|
||||
});
|
||||
const nextMessages = sortConversationMessages([...mergedServerMessages, ...unmatchedLocalMessages]);
|
||||
|
||||
return areChatMessagesEquivalent(previous, nextMessages) ? previous : nextMessages;
|
||||
@@ -1866,6 +2060,11 @@ export async function handleChatServerEvent({
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'notification:messages-updated') {
|
||||
notifyNotificationMessagesUpdated();
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'chat:error') {
|
||||
setMessages((previous) => [...previous, createLocalMessage(payload.payload.message)]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user