chore: exclude local resource artifacts from main sync

This commit is contained in:
2026-05-15 10:16:45 +09:00
parent 442879313f
commit d38d022872
504 changed files with 17074 additions and 3642 deletions

View File

@@ -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)]);
}