chore: sync local workspace changes
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
import { Alert, Button, Checkbox, Input, Select, Spin, message } from 'antd';
|
||||
import type { TextAreaRef } from 'antd/es/input/TextArea';
|
||||
import {
|
||||
startTransition,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
@@ -33,12 +34,17 @@ import {
|
||||
import { InlineImage } from '../../../components/common/InlineImage';
|
||||
import { CodexDiffBlock } from '../../../components/previewer';
|
||||
import type { ComposerFilePickResult } from '../chatV2/hooks/useConversationComposerController';
|
||||
import { ChatPreviewBody, resolveChatPreviewKindLabel, type ChatPreviewKind } from './ChatPreviewBody';
|
||||
import {
|
||||
ChatPreviewBody,
|
||||
resolveChatPreviewGlyph,
|
||||
resolveChatPreviewKindLabel,
|
||||
type ChatPreviewKind,
|
||||
} from './ChatPreviewBody';
|
||||
import { triggerResourceDownload } from './downloadUtils';
|
||||
import { extractAutoDetectedPreviewUrls } from './inlinePreviewUrls';
|
||||
import { extractHiddenPreviewUrls, stripHiddenPreviewTags } from './previewMarkers';
|
||||
import { normalizeChatResourceUrl } from './chatResourceUrl';
|
||||
import { copyPreviewContent, copyText } from './chatUtils';
|
||||
import { copyPreviewContent, copyText, isExecutionFailureMessage, isMissingRequestMessage, isPreparingChatReplyText } from './chatUtils';
|
||||
import { ChatLinkCardPreview } from './ChatLinkCardPreview';
|
||||
import { openChatExternalLink } from './linkNavigation';
|
||||
import { ChatRankedLinkPreview, type RankedLinkPreviewTarget } from './ChatRankedLinkPreview';
|
||||
@@ -110,6 +116,8 @@ type PreviewFetchError = Error & {
|
||||
const MARKDOWN_IMAGE_LINE_PATTERN = /^\s*!\[([^\]]*)\]\(([^)]+)\)\s*$/;
|
||||
const MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||
const CHAT_ACTIVITY_MESSAGE_PREFIX = '[[activity-log]]';
|
||||
const CHAT_MISSING_REQUEST_MESSAGE_PREFIX = '[[missing-request]]';
|
||||
const CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX = '[[execution-failure]]';
|
||||
const COLLAPSIBLE_MESSAGE_LINE_COUNT = 6;
|
||||
const COLLAPSIBLE_MESSAGE_CHAR_COUNT = 280;
|
||||
const DIFF_CODE_BLOCK_PATTERN = /```diff[^\n]*\n([\s\S]*?)```/g;
|
||||
@@ -199,6 +207,25 @@ function buildPreviewFileName(item: Pick<PreviewOption, 'url' | 'label'>) {
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePreviewFileExtension(item: Pick<PreviewOption, 'url' | 'label'>) {
|
||||
const fileName = buildPreviewFileName(item).toLowerCase();
|
||||
const match = fileName.match(/\.([a-z0-9]{1,16})$/i);
|
||||
return match?.[1] ?? '';
|
||||
}
|
||||
|
||||
function buildResourceChipMeta(item: Pick<PreviewOption, 'url' | 'label' | 'kind'>) {
|
||||
const extension = resolvePreviewFileExtension(item);
|
||||
|
||||
if (extension) {
|
||||
return extension.toUpperCase();
|
||||
}
|
||||
|
||||
return resolveChatPreviewKindLabel(item.kind as ChatPreviewKind)
|
||||
.replace(/\s+preview$/i, '')
|
||||
.replace(/\s+download$/i, '')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
function normalizeRankedLinkTitle(value: string) {
|
||||
return value
|
||||
.replace(/^\[(.+)\]\([^)]+\)$/u, '$1')
|
||||
@@ -561,6 +588,22 @@ function isActivityLogMessage(message: ChatMessage) {
|
||||
return message.author === 'system' && message.text.startsWith(`${CHAT_ACTIVITY_MESSAGE_PREFIX}\n`);
|
||||
}
|
||||
|
||||
function getMissingRequestMessageText(message: ChatMessage) {
|
||||
if (!isMissingRequestMessage(message)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return message.text.slice(`${CHAT_MISSING_REQUEST_MESSAGE_PREFIX}\n`.length).trim();
|
||||
}
|
||||
|
||||
function getExecutionFailureMessageText(message: ChatMessage) {
|
||||
if (!isExecutionFailureMessage(message)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return message.text.slice(`${CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX}\n`.length).trim();
|
||||
}
|
||||
|
||||
function extractActivityLines(message: ChatMessage) {
|
||||
return message.text
|
||||
.slice(`${CHAT_ACTIVITY_MESSAGE_PREFIX}\n`.length)
|
||||
@@ -570,8 +613,20 @@ function extractActivityLines(message: ChatMessage) {
|
||||
}
|
||||
|
||||
function summarizeActivityLines(lines: string[]) {
|
||||
const latestLine = lines.at(-1) ?? '';
|
||||
return latestLine;
|
||||
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
||||
const summary = lines[index]
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.startsWith('# 이유:') || line.startsWith('# 진행:') || line.startsWith('# 상태:'));
|
||||
|
||||
if (!summary) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return summary.replace(/^#\s*(이유|진행|상태):\s*/u, '').trim();
|
||||
}
|
||||
|
||||
return lines.at(-1) ?? '';
|
||||
}
|
||||
|
||||
function isLikelyCollapsibleMessage(text: string) {
|
||||
@@ -948,8 +1003,8 @@ type ChatConversationViewProps = {
|
||||
onPickComposerFiles: (files: File[]) => ComposerFilePickResult | Promise<ComposerFilePickResult>;
|
||||
onRemoveComposerAttachment: (attachmentId: string) => void;
|
||||
onSelectChatType: (value: string) => void;
|
||||
onSend: () => void;
|
||||
onSendImmediate: () => void;
|
||||
onSend: (draftText?: string) => void;
|
||||
onSendImmediate: (draftText?: string) => void;
|
||||
onToggleSendWithoutContext: () => void;
|
||||
onClearDraft: () => void;
|
||||
onScrollToBottom: () => void;
|
||||
@@ -1018,12 +1073,58 @@ export function ChatConversationView({
|
||||
const [showBusyOverlay, setShowBusyOverlay] = useState(false);
|
||||
const [showLatestResourceOnly, setShowLatestResourceOnly] = useState(true);
|
||||
const [pendingComposerUploads, setPendingComposerUploads] = useState<PendingComposerUpload[]>([]);
|
||||
const [composerDraft, setComposerDraft] = useState(draft);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const activitySectionRefs = useRef(new Map<string, HTMLElement>());
|
||||
const messageBodyRefs = useRef(new Map<number, HTMLDivElement>());
|
||||
const autoCollapsedActivityRequestIdsRef = useRef(new Set<string>());
|
||||
const lastReportedDraftRef = useRef(draft);
|
||||
|
||||
useEffect(() => {
|
||||
if (draft === lastReportedDraftRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setComposerDraft(draft);
|
||||
}, [draft]);
|
||||
|
||||
useEffect(() => {
|
||||
if (composerDraft === lastReportedDraftRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
lastReportedDraftRef.current = composerDraft;
|
||||
startTransition(() => {
|
||||
onDraftChange(composerDraft);
|
||||
});
|
||||
}, 120);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [composerDraft, onDraftChange]);
|
||||
|
||||
const orderedMessages = useMemo(() => {
|
||||
const shouldDisplayActivityMessage = (activityMessage: ChatMessage) => {
|
||||
const requestId = activityMessage.clientRequestId?.trim();
|
||||
|
||||
if (!requestId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const requestState = requestStateMap.get(requestId);
|
||||
const hasCodexResponse = visibleMessages.some(
|
||||
(candidate) =>
|
||||
candidate.clientRequestId?.trim() === requestId &&
|
||||
candidate.author === 'codex' &&
|
||||
candidate.text.trim().length > 0 &&
|
||||
!isPreparingChatReplyText(candidate.text),
|
||||
);
|
||||
|
||||
return !isTerminalRequestStatus(requestState?.status) || !hasCodexResponse;
|
||||
};
|
||||
|
||||
const latestActivityByRequestId = new Map<string, ChatMessage>();
|
||||
const orphanActivityMessages: ChatMessage[] = [];
|
||||
const baseMessages = visibleMessages.filter((message) => {
|
||||
@@ -1038,7 +1139,9 @@ export function ChatConversationView({
|
||||
return false;
|
||||
}
|
||||
|
||||
latestActivityByRequestId.set(activityKey, message);
|
||||
if (shouldDisplayActivityMessage(message)) {
|
||||
latestActivityByRequestId.set(activityKey, message);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const insertedActivityRequestIds = new Set<string>();
|
||||
@@ -1074,19 +1177,9 @@ export function ChatConversationView({
|
||||
});
|
||||
|
||||
return [...ordered, ...orphanActivityMessages];
|
||||
}, [visibleMessages]);
|
||||
}, [requestStateMap, visibleMessages]);
|
||||
const previewItemsByUrl = useMemo(() => new Map(previewItems.map((item) => [item.url, item])), [previewItems]);
|
||||
const isChatTypeReadonly = useMemo(() => {
|
||||
if (isChatTypeSelectionLocked) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(new URLSearchParams(window.location.search).get('sessionId')?.trim());
|
||||
}, [isChatTypeSelectionLocked]);
|
||||
const isChatTypeReadonly = isChatTypeSelectionLocked;
|
||||
const visiblePreviewItems = useMemo(() => {
|
||||
if (!showLatestResourceOnly) {
|
||||
return previewItems;
|
||||
@@ -1590,8 +1683,20 @@ export function ChatConversationView({
|
||||
onOpenPreview(item.id);
|
||||
}}
|
||||
>
|
||||
<span title={item.label}>{item.label}</span>
|
||||
<span>{item.kind}</span>
|
||||
<span className="app-chat-panel__resource-chip-main">
|
||||
<span className="app-chat-panel__resource-chip-icon" aria-hidden="true">
|
||||
{resolveChatPreviewGlyph(item.kind as ChatPreviewKind)}
|
||||
</span>
|
||||
<span className="app-chat-panel__resource-chip-label" title={item.label}>
|
||||
{item.label}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="app-chat-panel__resource-chip-meta"
|
||||
aria-label={`${resolveChatPreviewKindLabel(item.kind as ChatPreviewKind)} 형식`}
|
||||
>
|
||||
{buildResourceChipMeta(item)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -1638,8 +1743,17 @@ export function ChatConversationView({
|
||||
const canCollapseMessage = collapsibleMessageIds.includes(message.id);
|
||||
const isExpandedMessage = expandedMessageIds.includes(message.id);
|
||||
const shouldTruncateMessage = canCollapseMessage && !isExpandedMessage;
|
||||
const messageBodyClassName = `app-chat-message__body${shouldTruncateMessage ? ' app-chat-message__body--collapsed' : ''}`;
|
||||
const isRecoveredMissingRequest = isMissingRequestMessage(message);
|
||||
const isRecoveredExecutionFailure = isExecutionFailureMessage(message);
|
||||
const baseMessageBodyClassName = `app-chat-message__body${shouldTruncateMessage ? ' app-chat-message__body--collapsed' : ''}${
|
||||
isRecoveredMissingRequest || isRecoveredExecutionFailure ? ' app-chat-message__body--system-status' : ''
|
||||
}`;
|
||||
const { previewSourceText, visibleText, diffBlocks, rankedLinkTargets, linkCardTargets } = extractMessageRenderPayload(message);
|
||||
const renderedText = isRecoveredMissingRequest
|
||||
? getMissingRequestMessageText(message)
|
||||
: isRecoveredExecutionFailure
|
||||
? getExecutionFailureMessageText(message)
|
||||
: visibleText;
|
||||
|
||||
if (isActivityLogMessage(message)) {
|
||||
return renderActivityCard(message);
|
||||
@@ -1651,9 +1765,9 @@ export function ChatConversationView({
|
||||
const shouldRenderStandalonePreview =
|
||||
hasPreviewCards && !visibleText && (message.author === 'codex' || message.author === 'system');
|
||||
const stackClassName = [
|
||||
`app-chat-message-stack app-chat-message-stack--${message.author}`,
|
||||
shouldRenderStandalonePreview ? 'app-chat-message-stack--artifact-only' : '',
|
||||
]
|
||||
`app-chat-message-stack app-chat-message-stack--${message.author}`,
|
||||
shouldRenderStandalonePreview ? 'app-chat-message-stack--artifact-only' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const requestState = message.clientRequestId ? requestStateMap.get(message.clientRequestId) : undefined;
|
||||
@@ -1663,10 +1777,26 @@ export function ChatConversationView({
|
||||
return (
|
||||
<div key={message.id} className={stackClassName}>
|
||||
{shouldRenderStandalonePreview ? null : (
|
||||
<article className={`app-chat-message app-chat-message--${message.author}`}>
|
||||
<article
|
||||
className={`app-chat-message ${
|
||||
isRecoveredMissingRequest || isRecoveredExecutionFailure
|
||||
? 'app-chat-message--system-inline'
|
||||
: `app-chat-message--${message.author}`
|
||||
}`}
|
||||
>
|
||||
<div className="app-chat-message__header">
|
||||
<div className="app-chat-message__header-meta">
|
||||
<strong>{message.author === 'codex' ? 'Codex' : message.author === 'user' ? 'You' : 'System'}</strong>
|
||||
<strong>
|
||||
{isRecoveredMissingRequest
|
||||
? '원문 누락'
|
||||
: isRecoveredExecutionFailure
|
||||
? '실행 실패'
|
||||
: message.author === 'codex'
|
||||
? 'Codex'
|
||||
: message.author === 'user'
|
||||
? 'You'
|
||||
: 'System'}
|
||||
</strong>
|
||||
<span>{formatChatTimestamp(message.timestamp)}</span>
|
||||
{message.author === 'user' && requestStatusLabel ? (
|
||||
<span className="app-chat-message__status" aria-label={`요청 상태 ${requestStatusLabel}`}>
|
||||
@@ -1743,13 +1873,13 @@ export function ChatConversationView({
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
ref={(element) => {
|
||||
setMessageBodyRef(message.id, element);
|
||||
}}
|
||||
className={messageBodyClassName}
|
||||
>
|
||||
{visibleText ? renderMessageBody(visibleText) : null}
|
||||
<div
|
||||
ref={(element) => {
|
||||
setMessageBodyRef(message.id, element);
|
||||
}}
|
||||
className={baseMessageBodyClassName}
|
||||
>
|
||||
{renderedText ? renderMessageBody(renderedText) : null}
|
||||
</div>
|
||||
{message.author === 'user' && requestDetailText ? (
|
||||
<div className="app-chat-message__request-detail" role="status" aria-live="polite">
|
||||
@@ -1917,22 +2047,38 @@ export function ChatConversationView({
|
||||
isSendWithoutContextEnabled ? ' app-chat-panel__composer-contextless-toggle--active' : ''
|
||||
}`}
|
||||
icon={<DisconnectOutlined />}
|
||||
aria-label={isSendWithoutContextEnabled ? '이전 문맥 없이 보내기 켜짐' : '이전 문맥 없이 보내기 꺼짐'}
|
||||
title={isSendWithoutContextEnabled ? '이전 문맥 없이 보내기 켜짐' : '이전 문맥 없이 보내기 꺼짐'}
|
||||
aria-label={
|
||||
isSendWithoutContextEnabled
|
||||
? '다음 1회만 문맥 없이 보냄'
|
||||
: '다음 전송을 문맥 없이 보내기'
|
||||
}
|
||||
title={
|
||||
isSendWithoutContextEnabled
|
||||
? '다음 1회만 문맥 없이 보냄'
|
||||
: '다음 전송을 문맥 없이 보내기'
|
||||
}
|
||||
onClick={onToggleSendWithoutContext}
|
||||
disabled={isComposerDisabled || isComposerAttachmentUploading}
|
||||
/>
|
||||
<Button
|
||||
icon={<ThunderboltOutlined />}
|
||||
aria-label="즉시 요청"
|
||||
onClick={onSendImmediate}
|
||||
onClick={() => {
|
||||
lastReportedDraftRef.current = composerDraft;
|
||||
onDraftChange(composerDraft);
|
||||
onSendImmediate(composerDraft);
|
||||
}}
|
||||
disabled={isComposerDisabled || isComposerAttachmentUploading}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
aria-label="큐로 보내기"
|
||||
onClick={onSend}
|
||||
onClick={() => {
|
||||
lastReportedDraftRef.current = composerDraft;
|
||||
onDraftChange(composerDraft);
|
||||
onSend(composerDraft);
|
||||
}}
|
||||
disabled={isComposerDisabled || isComposerAttachmentUploading}
|
||||
/>
|
||||
</div>
|
||||
@@ -1987,12 +2133,12 @@ export function ChatConversationView({
|
||||
|
||||
<Input.TextArea
|
||||
ref={composerRef}
|
||||
value={draft}
|
||||
value={composerDraft}
|
||||
autoSize={false}
|
||||
placeholder={composerPlaceholder}
|
||||
disabled={isComposerDisabled}
|
||||
onChange={(event) => {
|
||||
onDraftChange(event.target.value);
|
||||
setComposerDraft(event.target.value);
|
||||
}}
|
||||
onPaste={handleComposerPaste}
|
||||
onKeyDown={(event) => {
|
||||
@@ -2012,16 +2158,18 @@ export function ChatConversationView({
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onSend();
|
||||
lastReportedDraftRef.current = event.currentTarget.value;
|
||||
onDraftChange(event.currentTarget.value);
|
||||
onSend(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className={`app-chat-panel__composer-clear${draft.trim() ? ' app-chat-panel__composer-clear--visible' : ''}`}
|
||||
className={`app-chat-panel__composer-clear${composerDraft.trim() ? ' app-chat-panel__composer-clear--visible' : ''}`}
|
||||
aria-label="입력창 비우기"
|
||||
onClick={onClearDraft}
|
||||
disabled={!draft.trim()}
|
||||
disabled={!composerDraft.trim()}
|
||||
>
|
||||
clear
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user