chore: test deploy snapshot

This commit is contained in:
2026-05-27 10:43:01 +09:00
parent c1d0f4c1db
commit 4c4b3c8d2c
78 changed files with 10392 additions and 2301 deletions

View File

@@ -617,6 +617,10 @@ function resolvePromptSubmissionParentRequestId(
return resolveChildComposerParentRequestId(request, requestStateMap) || normalizedRequestId;
}
function isPromptFollowupRequest(request: ChatConversationRequest) {
return request.requestOrigin === 'prompt';
}
function resolveConversationMessageGroupRequestId(
requestId: string | null | undefined,
requestStateMap: Map<string, ChatConversationRequest>,
@@ -1155,6 +1159,10 @@ function collapseDuplicatedLeadingExecutorHeaders(text: string) {
return collapsedLines.join('\n');
}
function isRequestInFlightStatus(status: ChatConversationRequestStatus | null | undefined) {
return status === 'accepted' || status === 'queued' || status === 'started';
}
function extractMessageRenderPayload(message: ChatMessage): MessageRenderPayload {
const structuredParts = Array.isArray(message.parts) ? message.parts : [];
const attachmentExtraction = extractAttachmentPreviewUrls(message.text);
@@ -1752,6 +1760,9 @@ function formatRequestStatusLabel(
},
) {
const hideFinalizedLabel = options?.hideFinalizedLabel === true;
const retryCount = Math.max(0, Number(request?.retryCount ?? 0) || 0);
const appendRetryLabel = (label: string | null) =>
label && retryCount > 0 ? `${label} · 재처리 ${retryCount}` : label;
if (hasAnsweredRequest(request)) {
if (request?.status === "completed") {
@@ -1759,27 +1770,29 @@ function formatRequestStatusLabel(
return null;
}
return attentionState?.hasPendingPromptBadge || attentionState?.hasPendingVerificationBadge ? "확인대기" : "완료";
return appendRetryLabel(
attentionState?.hasPendingPromptBadge || attentionState?.hasPendingVerificationBadge ? "확인대기" : "완료",
);
}
return hideFinalizedLabel ? null : "답변도착";
return appendRetryLabel(hideFinalizedLabel ? null : "답변도착");
}
switch (request?.status) {
case 'accepted':
return '접수됨';
return appendRetryLabel('접수됨');
case 'queued':
return '대기중';
return appendRetryLabel('대기중');
case 'started':
return request.hasResponse ? '응답작성중' : '처리중';
return appendRetryLabel(request.hasResponse ? '응답작성중' : '처리중');
case 'completed':
return '완료';
return appendRetryLabel('완료');
case 'failed':
return '실패';
return appendRetryLabel('실패');
case 'cancelled':
return '취소됨';
return appendRetryLabel('취소됨');
case 'removed':
return '삭제됨';
return appendRetryLabel('삭제됨');
default:
return null;
}
@@ -2144,6 +2157,20 @@ function getRequestDetailBadge(request: ChatConversationRequest | undefined): Sy
return null;
}
function getRequestRetryBadge(request: ChatConversationRequest | undefined): SystemExecutionBadge | null {
const retryCount = Math.max(0, Number(request?.retryCount ?? 0) || 0);
if (retryCount <= 0) {
return null;
}
return {
label: `재처리 ${retryCount}`,
shortLabel: `재처리 ${retryCount}`,
tone: 'processing',
};
}
function buildChecklistStageBadge(lines: string[], request?: ChatConversationRequest): SystemExecutionBadge | null {
const entries = buildChatActivityChecklistEntries(lines, request);
const activeEntry =
@@ -2186,6 +2213,7 @@ function buildPromptStateBadge(options: {
promptTargets: Extract<ChatMessagePart, { type: 'prompt' }>[];
submittedCount: number;
isManuallyCompleted?: boolean;
isPromptManuallyCompleted?: boolean;
}): SystemExecutionBadge | null {
const { promptTargets, submittedCount, isManuallyCompleted } = options;
@@ -2254,6 +2282,7 @@ function buildVerificationStateBadge(options: {
hasVerificationTarget: boolean;
hasConfirmedVerificationTarget: boolean;
isManuallyCompleted?: boolean;
isPromptManuallyCompleted?: boolean;
}): SystemExecutionBadge | null {
const {
request,
@@ -2262,23 +2291,32 @@ function buildVerificationStateBadge(options: {
hasVerificationTarget,
hasConfirmedVerificationTarget,
isManuallyCompleted,
isPromptManuallyCompleted,
} = options;
if (!request || promptTargets.length > 0 || !hasVerificationTarget) {
if (!request || isPromptManuallyCompleted || promptTargets.length > 0 || !hasVerificationTarget) {
return null;
}
const verificationText = [request.userText, request.responseText, ...activityLines].join('\n');
const usesVerificationLabel = VERIFICATION_REQUEST_PATTERN.test(verificationText);
const completedLabel = usesVerificationLabel ? '검증 확인' : '응답 확인';
const pendingLabel = usesVerificationLabel ? '검증 미확인' : '응답 미확인';
if (!usesVerificationLabel) {
if (!request.hasResponse && request.status !== 'completed') {
return null;
}
}
return isManuallyCompleted
? {
label: usesVerificationLabel ? '검증 확인' : '응답 확인',
label: completedLabel,
shortLabel: '확인',
tone: 'completed',
}
: {
label: usesVerificationLabel ? '검증 미확인' : '응답 미확인',
label: pendingLabel,
shortLabel: '미확인',
tone: 'attention',
};
@@ -2291,6 +2329,7 @@ function hasPendingVerificationState(options: {
hasVerificationTarget: boolean;
hasConfirmedVerificationTarget: boolean;
isManuallyCompleted?: boolean;
isPromptManuallyCompleted?: boolean;
}) {
return buildVerificationStateBadge(options)?.tone === 'attention';
}
@@ -3304,14 +3343,32 @@ const ChatComposerInput = memo(function ChatComposerInput({
function SharedRoomsRequestCard({
request,
attentionState,
canCompletePrompt = false,
canCompleteVerification = false,
canReplyToResponse = false,
isManualCompletionSaving = false,
isReplyReferenceActive = false,
onCompletePrompt,
onCompleteVerification,
onReplyToResponse,
onSelect,
}: {
request: ChatConversationRequest;
attentionState?: SystemExecutionAttentionState;
canCompletePrompt?: boolean;
canCompleteVerification?: boolean;
canReplyToResponse?: boolean;
isManualCompletionSaving?: boolean;
isReplyReferenceActive?: boolean;
onCompletePrompt?: (() => void) | null;
onCompleteVerification?: (() => void) | null;
onReplyToResponse?: (() => void) | null;
onSelect?: (() => void) | null;
}) {
const questionText = (request.userText ?? "").trim() || "-";
const answerText = (request.responseText ?? "").trim() || request.statusMessage?.trim() || "아직 답변이 없습니다.";
const requestStatusLabel = formatRequestStatusLabel(request);
const requestStatusLabel = formatRequestStatusLabel(request, attentionState);
return (
<section
@@ -3354,6 +3411,58 @@ function SharedRoomsRequestCard({
<div className="app-chat-bubble__content">{answerText}</div>
</div>
</div>
{canCompletePrompt || canCompleteVerification || canReplyToResponse ? (
<div className="app-chat-message__response-actions">
{canCompletePrompt ? (
<Button
type="text"
size="small"
className="app-chat-message__response-action"
icon={<CheckOutlined />}
loading={isManualCompletionSaving}
onClick={(event) => {
event.stopPropagation();
onCompletePrompt?.();
}}
>
</Button>
) : null}
{canCompleteVerification ? (
<Button
type="text"
size="small"
className="app-chat-message__response-action"
icon={<CheckOutlined />}
loading={isManualCompletionSaving}
onClick={(event) => {
event.stopPropagation();
onCompleteVerification?.();
}}
>
</Button>
) : null}
{canReplyToResponse ? (
<Button
type="text"
size="small"
className={[
'app-chat-message__response-action',
'app-chat-message__response-reply-button',
isReplyReferenceActive ? 'app-chat-message__response-reply-button--active' : '',
].filter(Boolean).join(' ')}
icon={<SendOutlined />}
onClick={(event) => {
event.stopPropagation();
onReplyToResponse?.();
}}
>
{isReplyReferenceActive ? '답변 참조 중' : '답변하기'}
</Button>
) : null}
</div>
) : null}
</section>
);
}
@@ -3480,9 +3589,7 @@ export function ChatConversationView({
const [systemExecutionDisplayMode, setSystemExecutionDisplayMode] = useState<SystemExecutionDisplayMode>('collapsed');
const [systemExecutionFilter, setSystemExecutionFilter] = useState<SystemExecutionFilter>('active-attention');
const [systemExecutionSort, setSystemExecutionSort] = useState<SystemExecutionSort>('latest');
const [roomShareExpandMode, setRoomShareExpandMode] = useState<RoomShareExpandMode>(
useRoomsShareBubbleFlow ? 'latest' : 'pending',
);
const [roomShareExpandMode, setRoomShareExpandMode] = useState<RoomShareExpandMode>('pending');
const [selectedRoomShareGroupId, setSelectedRoomShareGroupId] = useState<string | null>(null);
const [replyReferenceRequestId, setReplyReferenceRequestId] = useState('');
const [composerForceDraftSyncVersion, setComposerForceDraftSyncVersion] = useState(0);
@@ -3702,7 +3809,7 @@ export function ChatConversationView({
return [...ordered, ...orphanActivityMessages];
}, [requestStateMap, visibleMessages]);
useEffect(() => {
setRoomShareExpandMode(useRoomsShareBubbleFlow ? 'latest' : 'pending');
setRoomShareExpandMode('pending');
}, [useRoomsShareBubbleFlow]);
const useSharedRoomsSimplifiedView = showRoomsShareHeader && useSharedComposerChrome && !roomsShareUseSharedPageNav;
@@ -4104,7 +4211,8 @@ export function ChatConversationView({
() =>
visibleSystemExecutionRequests.filter(
(request) =>
request.status === 'accepted' || request.status === 'queued' || request.status === 'started',
!isPromptFollowupRequest(request)
&& (request.status === 'accepted' || request.status === 'queued' || request.status === 'started'),
),
[visibleSystemExecutionRequests],
);
@@ -4161,6 +4269,22 @@ export function ChatConversationView({
const nextMap = new Map<string, SystemExecutionAttentionState>();
visibleSystemExecutionRequests.forEach((request) => {
if (isPromptFollowupRequest(request)) {
nextMap.set(request.requestId, {
activityLines: [],
promptTargets: [],
promptSubmittedCount: 0,
isPromptManuallyCompleted: true,
hasVerificationTarget: false,
hasConfirmedVerificationTarget: true,
isVerificationManuallyCompleted: true,
hasPendingPromptBadge: false,
hasPendingVerificationBadge: false,
hasOwnAttentionState: false,
});
return;
}
const activityLines = activityLinesByRequestId.get(request.requestId) ?? [];
const promptTargets = promptTargetsByRequestId.get(request.requestId) ?? [];
const promptSubmittedCount =
@@ -4193,6 +4317,7 @@ export function ChatConversationView({
promptTargets,
hasVerificationTarget,
hasConfirmedVerificationTarget,
isPromptManuallyCompleted,
});
const hasOwnAttentionState =
hasPendingPromptBadge || hasPendingVerificationBadge || isDisconnectedRequestNeedingAttention(request);
@@ -4219,6 +4344,7 @@ export function ChatConversationView({
lastReadResponseMessageId,
openedPreviewArtifactRequestIdSet,
openedPreviewRequestIds,
childRequestIdsByParentRequestId,
localSubmittedPromptCountByRequestId,
promptFollowupCountByParentRequestId,
promptTargetsByRequestId,
@@ -4233,12 +4359,18 @@ export function ChatConversationView({
if (systemExecutionFilter === 'active') {
return visibleSystemExecutionRequests.filter(
(request) => request.status === 'accepted' || request.status === 'queued' || request.status === 'started',
(request) =>
!isPromptFollowupRequest(request)
&& (request.status === 'accepted' || request.status === 'queued' || request.status === 'started'),
);
}
if (systemExecutionFilter === 'active-attention') {
return visibleSystemExecutionRequests.filter((request) => {
if (isPromptFollowupRequest(request)) {
return false;
}
if (request.status === 'accepted' || request.status === 'queued' || request.status === 'started') {
return true;
}
@@ -4248,7 +4380,9 @@ export function ChatConversationView({
}
return visibleSystemExecutionRequests.filter(
(request) => systemExecutionAttentionStateByRequestId.get(request.requestId)?.hasOwnAttentionState === true,
(request) =>
!isPromptFollowupRequest(request)
&& systemExecutionAttentionStateByRequestId.get(request.requestId)?.hasOwnAttentionState === true,
);
},
[systemExecutionAttentionStateByRequestId, visibleSystemExecutionRequests, systemExecutionFilter],
@@ -4362,12 +4496,23 @@ export function ChatConversationView({
});
}
return messageEntries
const groupRequestIdsByRootId = new Map<string, string[]>();
requestIdsByRootRequestId.forEach((requestIds, rootRequestId) => {
const dedupedRequestIds = Array.from(new Set(requestIds.map((requestId) => requestId.trim()).filter(Boolean)));
if (dedupedRequestIds.length > 0) {
groupRequestIdsByRootId.set(rootRequestId, dedupedRequestIds);
}
});
const groupedEntries = messageEntries
.filter((entry): entry is Extract<ConversationMessageEntry, { kind: 'group' }> => entry.kind === 'group')
.map((entry) => {
const groupedRequestIds = groupRequestIdsByRootId.get(entry.groupId) ?? entry.requestIds;
const groupedRequests = Array.from(
new Map(
entry.requestIds
groupedRequestIds
.map((requestId) => requestStateMap.get(requestId))
.filter((item): item is ChatConversationRequest => item != null)
.map((request) => [request.requestId, request] as const),
@@ -4393,10 +4538,60 @@ export function ChatConversationView({
statusSummary,
hasAttention,
};
});
const existingGroupIdSet = new Set(groupedEntries.map((entry) => entry.groupId));
const pendingOnlyGroups = Array.from(groupRequestIdsByRootId.entries())
.filter(([groupId]) => !existingGroupIdSet.has(groupId))
.map(([groupId, groupedRequestIds]) => {
const groupedRequests = Array.from(
new Map(
groupedRequestIds
.map((requestId) => requestStateMap.get(requestId))
.filter((item): item is ChatConversationRequest => item != null)
.map((request) => [request.requestId, request] as const),
).values(),
);
if (groupedRequests.length === 0) {
return null;
}
const hasAttention = groupedRequests.some((request) => {
if (isRequestRunningStatus(request.status) || isRequestQueueStatus(request.status)) {
return true;
}
return systemExecutionAttentionStateByRequestId.get(request.requestId)?.hasOwnAttentionState === true;
});
if (!hasAttention) {
return null;
}
const representativeRequest = groupedRequests[groupedRequests.length - 1] ?? null;
return {
groupId,
groupedRequests,
representativeRequest,
statusSummary: resolveAggregatedRequestStatusSummary(groupedRequests, systemExecutionAttentionStateByRequestId),
hasAttention,
};
})
.filter((entry) => entry.representativeRequest != null);
.filter((entry): entry is NonNullable<typeof entry> => entry != null);
return [...groupedEntries, ...pendingOnlyGroups]
.filter((entry) => entry.representativeRequest != null)
.sort((left, right) => left.representativeRequest.createdAt.localeCompare(right.representativeRequest.createdAt));
},
[messageEntries, orderedMessages, requestStateMap, systemExecutionAttentionStateByRequestId, useSharedRoomsSimplifiedView],
[
messageEntries,
orderedMessages,
requestIdsByRootRequestId,
requestStateMap,
systemExecutionAttentionStateByRequestId,
useSharedRoomsSimplifiedView,
],
);
const roomShareNavigableGroups = useMemo(() => {
if (roomShareExpandMode === 'pending') {
@@ -4501,7 +4696,7 @@ export function ChatConversationView({
const nowMs = Date.now();
const processingRequests = roomShareAllRequests.filter(
(request) => isRequestRunningStatus(request.status) || isRequestQueueStatus(request.status),
);
).filter((request) => !isPromptFollowupRequest(request));
const processingTarget = processingRequests[processingRequests.length - 1] ?? null;
const elapsedLabel = processingTarget ? formatOngoingElapsedLabel(processingTarget.createdAt, nowMs) : '';
const processingCount = processingRequests.length;
@@ -5344,6 +5539,7 @@ export function ChatConversationView({
hasVerificationTarget,
hasConfirmedVerificationTarget,
isManuallyCompleted: isVerificationManuallyCompleted,
isPromptManuallyCompleted,
});
const hasPendingVerificationBadge = attentionState?.hasPendingVerificationBadge === true;
const manualCompletionTypes = buildManualCompletionTypes({
@@ -5353,9 +5549,11 @@ export function ChatConversationView({
const checklistBadge = buildChecklistStageBadge(activityLines, representativeRequest);
const readStateBadge =
verificationStateBadge ? null : buildReadStateBadge(representativeRequest, lastReadResponseMessageId);
const retryBadge = getRequestRetryBadge(representativeRequest);
const secondaryBadges = [
hierarchyBadge,
rootRelationshipBadge,
retryBadge,
detailBadge,
promptStateBadge,
verificationStateBadge,
@@ -5812,6 +6010,25 @@ export function ChatConversationView({
}, 0);
};
const activateReplyReference = (
requestId: string | null | undefined,
options?: {
focusComposer?: boolean;
},
) => {
const normalizedRequestId = requestId?.trim() || '';
if (!normalizedRequestId) {
return;
}
setReplyReferenceRequestId(normalizedRequestId);
if (options?.focusComposer !== false) {
composerRef.current?.focus({ cursor: 'end' });
}
};
const closeChildComposer = (groupId: string) => {
const normalizedGroupId = groupId.trim();
@@ -5959,13 +6176,19 @@ export function ChatConversationView({
hasVerificationTarget: responseHasVerificationTarget,
hasConfirmedVerificationTarget: responseHasConfirmedVerificationTarget,
isManuallyCompleted: isResponseVerificationManuallyCompleted,
isPromptManuallyCompleted: isResponsePromptManuallyCompleted,
});
const responseHasPendingVerificationBadge = attentionState?.hasPendingVerificationBadge === true;
const responseSecondaryBadges = [responsePromptStateBadge, responseVerificationStateBadge].filter(
(badge): badge is SystemExecutionBadge => badge != null,
);
const responseRetryBadge = getRequestRetryBadge(requestState);
const responseDisplayBadges = [
...(responseRetryBadge ? [responseRetryBadge] : []),
...responseSecondaryBadges,
];
const responsePrimaryManualCompletionType = resolvePrimaryManualCompletionType({
secondaryBadges: responseSecondaryBadges,
secondaryBadges: responseDisplayBadges,
promptStateBadge: responsePromptStateBadge,
verificationStateBadge: responseVerificationStateBadge,
hasPendingPromptBadge: responseHasPendingPromptBadge,
@@ -5991,9 +6214,6 @@ export function ChatConversationView({
promptSignature: buildPromptTargetSignature(promptTargets[0]!),
}
: null;
const hasChildRequest =
Boolean(message.clientRequestId) &&
(childRequestIdsByParentRequestId.get(message.clientRequestId ?? '')?.length ?? 0) > 0;
const showResponseManualCompleteAction =
enableExecutionReviewUi &&
message.author === 'codex' &&
@@ -6004,19 +6224,21 @@ export function ChatConversationView({
message.author === 'codex' &&
Boolean(message.clientRequestId) &&
responsePromptTargets.length > 0 &&
!isResponsePromptManuallyCompleted;
!isResponsePromptManuallyCompleted &&
!isRequestInFlightStatus(requestState?.status);
const canCompleteVerificationFromResponse =
showRoomsShareHeader &&
message.author === 'codex' &&
Boolean(message.clientRequestId) &&
responsePromptTargets.length === 0 &&
responsePrimaryManualCompletionType === 'verification' &&
!hasChildRequest;
!isRequestInFlightStatus(requestState?.status);
const canReplyToResponse =
showRoomsShareHeader &&
message.author === 'codex' &&
Boolean(message.clientRequestId) &&
responsePromptTargets.length === 0;
responsePromptTargets.length === 0 &&
!isRequestInFlightStatus(requestState?.status);
const isReplyReferenceActive =
canReplyToResponse &&
replyReferenceRequestId.trim() === (message.clientRequestId?.trim() ?? '');
@@ -6247,8 +6469,9 @@ export function ChatConversationView({
className={['app-chat-message__response-action', 'app-chat-message__response-reply-button', isReplyReferenceActive ? 'app-chat-message__response-reply-button--active' : ''].filter(Boolean).join(' ')}
icon={<SendOutlined />}
onClick={() => {
setReplyReferenceRequestId(message.clientRequestId?.trim() ?? '');
composerRef.current?.focus({ cursor: 'end' });
activateReplyReference(message.clientRequestId, {
focusComposer: false,
});
}}
>
{isReplyReferenceActive ? '답변 참조 중' : '답변하기'}
@@ -6997,6 +7220,44 @@ export function ChatConversationView({
const visibleMessages = Array.isArray(entry.messages) ? entry.messages.filter((message) => !isActivityLogMessage(message)) : [];
const collapsedVisibleFinalMessage =
visibleMessages.length > 0 ? visibleMessages[visibleMessages.length - 1] : null;
const representativeAttentionState = representativeRequest
? systemExecutionAttentionStateByRequestId.get(representativeRequest.requestId)
: undefined;
const representativePromptTargets = representativeAttentionState?.promptTargets ?? [];
const isRepresentativePromptManuallyCompleted =
representativeAttentionState?.isPromptManuallyCompleted === true;
const representativeHasPendingPromptBadge =
representativeAttentionState?.hasPendingPromptBadge === true;
const representativeHasPendingVerificationBadge =
representativeAttentionState?.hasPendingVerificationBadge === true;
const representativeManualCompletionTypes = buildManualCompletionTypes({
hasPendingPromptBadge: representativeHasPendingPromptBadge,
hasPendingVerificationBadge: representativeHasPendingVerificationBadge,
});
const isRepresentativeManualCompletionSaving =
representativeRequest != null &&
representativeManualCompletionTypes.some((type) =>
pendingManualCompletionActionKeySet.has(`${type}:${representativeRequest.requestId}`),
);
const canCompletePromptFromCard =
representativeRequest != null &&
representativePromptTargets.length > 0 &&
!isRepresentativePromptManuallyCompleted &&
!isRequestInFlightStatus(representativeRequest.status);
const canCompleteVerificationFromCard =
representativeRequest != null &&
representativePromptTargets.length === 0 &&
representativeHasPendingVerificationBadge &&
!isRequestInFlightStatus(representativeRequest.status);
const canReplyToCardResponse =
representativeRequest != null &&
representativeRequest.hasResponse &&
representativePromptTargets.length === 0 &&
!isRequestInFlightStatus(representativeRequest.status);
const isCardReplyReferenceActive =
representativeRequest != null &&
canReplyToCardResponse &&
replyReferenceRequestId.trim() === representativeRequest.requestId.trim();
if (!representativeRequest) {
return null;
@@ -7011,6 +7272,29 @@ export function ChatConversationView({
<SharedRoomsRequestCard
key={entry.groupId}
request={representativeRequest}
attentionState={representativeAttentionState}
canCompletePrompt={canCompletePromptFromCard}
canCompleteVerification={canCompleteVerificationFromCard}
canReplyToResponse={canReplyToCardResponse}
isManualCompletionSaving={isRepresentativeManualCompletionSaving}
isReplyReferenceActive={isCardReplyReferenceActive}
onCompletePrompt={() => {
void completeManualBadge({
requestId: representativeRequest.requestId,
type: 'prompt',
});
}}
onCompleteVerification={() => {
void completeManualBadge({
requestId: representativeRequest.requestId,
type: 'verification',
});
}}
onReplyToResponse={() => {
activateReplyReference(representativeRequest.requestId, {
focusComposer: false,
});
}}
onSelect={() => {
setSelectedRoomShareGroupId(entry.groupId);
scrollToRoomShareGroup(entry.groupId);
@@ -7113,9 +7397,7 @@ export function ChatConversationView({
const parentRequestId = entry.request?.parentRequestId?.trim() || '';
const parentRequest = parentRequestId ? requestStateMap.get(parentRequestId) : null;
const groupRelationText = parentRequest
? `${
entry.request?.requestOrigin === 'prompt' ? '상위 질의' : '상위 요청 연결'
}: ${summarizeQueuedText(parentRequest.userText || parentRequest.responseText || parentRequestId, 52)}`
? `부모 요청: ${summarizeQueuedText(parentRequest.userText || parentRequest.responseText || parentRequestId, 52)}`
: null;
const childComposerGroupId = entry.groupId;
const childComposerParentRequestId = resolveChildComposerParentRequestId(entry.request, requestStateMap);