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

@@ -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>

View File

@@ -0,0 +1,191 @@
import { Typography } from 'antd';
import type { ChatPreviewTarget } from './ChatPreviewBody';
const { Text } = Typography;
type TableCellValue = string;
type TabularPreviewModel = {
columns: string[];
rows: TableCellValue[][];
rowCount: number;
sourceLabel: string;
};
type ChatDataTablePreviewProps = {
model: TabularPreviewModel;
};
function stringifyCellValue(value: unknown): TableCellValue {
if (value === null || value === undefined) {
return '';
}
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function normalizeObjectRows(rows: Record<string, unknown>[], sourceLabel: string): TabularPreviewModel | null {
if (!rows.length) {
return null;
}
const columns = Array.from(
rows.reduce((set, row) => {
Object.keys(row).forEach((key) => set.add(key));
return set;
}, new Set<string>()),
);
if (!columns.length) {
return null;
}
return {
columns,
rows: rows.map((row) => columns.map((column) => stringifyCellValue(row[column]))),
rowCount: rows.length,
sourceLabel,
};
}
function resolveJsonRows(value: unknown): Record<string, unknown>[] | null {
if (Array.isArray(value) && value.every((item) => item && typeof item === 'object' && !Array.isArray(item))) {
return value as Record<string, unknown>[];
}
if (value && typeof value === 'object' && !Array.isArray(value)) {
const entries = Object.values(value as Record<string, unknown>);
for (const entry of entries) {
const resolved = resolveJsonRows(entry);
if (resolved?.length) {
return resolved;
}
}
}
return null;
}
function parseCsvLine(line: string) {
const cells: string[] = [];
let current = '';
let quoted = false;
for (let index = 0; index < line.length; index += 1) {
const char = line[index] ?? '';
const nextChar = line[index + 1] ?? '';
if (char === '"') {
if (quoted && nextChar === '"') {
current += '"';
index += 1;
continue;
}
quoted = !quoted;
continue;
}
if (char === ',' && !quoted) {
cells.push(current.trim());
current = '';
continue;
}
current += char;
}
cells.push(current.trim());
return cells;
}
function parseCsvTable(previewText: string, sourceLabel: string): TabularPreviewModel | null {
const lines = previewText
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
if (lines.length < 2) {
return null;
}
const header = parseCsvLine(lines[0] ?? '');
if (!header.length || header.every((column) => !column)) {
return null;
}
const rows = lines.slice(1).map((line) => {
const parsed = parseCsvLine(line);
return header.map((_, index) => parsed[index] ?? '');
});
return {
columns: header,
rows,
rowCount: rows.length,
sourceLabel,
};
}
export function resolveTabularPreviewModel(target: ChatPreviewTarget, previewText: string): TabularPreviewModel | null {
const pathname = target.url.toLowerCase().split('?')[0] ?? '';
if (pathname.endsWith('.json')) {
try {
const parsed = JSON.parse(previewText) as unknown;
const rows = resolveJsonRows(parsed);
return rows ? normalizeObjectRows(rows, target.label) : null;
} catch {
return null;
}
}
if (pathname.endsWith('.csv')) {
return parseCsvTable(previewText, target.label);
}
return null;
}
export function ChatDataTablePreview({ model }: ChatDataTablePreviewProps) {
return (
<div className="app-chat-panel__preview-table">
<div className="app-chat-panel__preview-table-meta">
<Text strong>{model.sourceLabel}</Text>
<Text type="secondary">{`${model.rowCount}개 · 열 ${model.columns.length}`}</Text>
</div>
<div className="app-chat-panel__preview-table-scroll">
<table className="app-chat-panel__preview-table-grid">
<thead>
<tr>
{model.columns.map((column) => (
<th key={column}>{column}</th>
))}
</tr>
</thead>
<tbody>
{model.rows.map((row, rowIndex) => (
<tr key={`${model.sourceLabel}-${rowIndex}`}>
{row.map((cell, cellIndex) => (
<td key={`${model.columns[cellIndex] ?? cellIndex}-${rowIndex}`}>{cell || '-'}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

View File

@@ -14,6 +14,7 @@ import { InlineImage } from '../../../components/common/InlineImage';
import { MarkdownPreviewContent } from '../../../components/markdownPreview/MarkdownPreviewContent';
import { CodexDiffBlock } from '../../../components/previewer';
import { inferCodeLanguage, renderEditorBlock } from '../../../components/previewer/renderers';
import { ChatDataTablePreview, resolveTabularPreviewModel } from './ChatDataTablePreview';
import { triggerResourceDownload } from './downloadUtils';
import '../../../components/previewer/PreviewerUI.css';
@@ -359,6 +360,12 @@ export function ChatPreviewBody({
}
if (target.kind === 'code' || target.kind === 'diff' || target.kind === 'document') {
const tabularModel = resolveTabularPreviewModel(target, previewText);
if (tabularModel) {
return <ChatDataTablePreview model={tabularModel} />;
}
const resolvedLanguage = resolveCodeLanguage(target, previewText);
if (renderHtmlAsFrame && resolvedLanguage === 'html' && canRenderFramePreview(target.url)) {

View File

@@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.normalizeChatResourceUrl = normalizeChatResourceUrl;
var CHAT_API_RESOURCE_MARKER = '/api/chat/resources/';
var CHAT_PUBLIC_RESOURCE_MARKER = '/.codex_chat/';
function extractEmbeddedResourcePath(value) {
var normalized = String(value !== null && value !== void 0 ? value : '').trim();
if (!normalized) {
return '';
}
var apiMarkerIndex = normalized.lastIndexOf(CHAT_API_RESOURCE_MARKER);
if (apiMarkerIndex >= 0) {
return normalized.slice(apiMarkerIndex);
}
var publicMarkerIndex = normalized.lastIndexOf(CHAT_PUBLIC_RESOURCE_MARKER);
if (publicMarkerIndex >= 0) {
return normalized.slice(publicMarkerIndex);
}
return normalized;
}
function normalizeChatResourceUrl(value) {
var normalized = extractEmbeddedResourcePath(value);
if (typeof window === 'undefined') {
return normalized;
}
try {
return new URL(normalized, window.location.href).toString();
}
catch (_a) {
return normalized;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,8 @@ const CHAT_NOTIFY_OFFLINE_STORAGE_PREFIX = 'main-chat-panel:notify-offline:';
const CHAT_SESSION_LAST_TYPE_STORAGE_PREFIX = 'main-chat-panel:last-chat-type:';
const CHAT_INTRO_MESSAGE = '요청은 기본적으로 순차 처리됩니다. 급한 요청만 즉시 실행을 사용하세요.';
const CHAT_ACTIVITY_MESSAGE_PREFIX = '[[activity-log]]';
const CHAT_MISSING_REQUEST_MESSAGE_PREFIX = '[[missing-request]]';
const CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX = '[[execution-failure]]';
const CHAT_COMPOSER_UPLOAD_FILE_SIZE_LIMIT = 10 * 1024 * 1024;
const KST_TIME_ZONE = 'Asia/Seoul';
const chatSessionLastTypeMemory = new Map<string, string>();
@@ -46,18 +48,23 @@ function toConversationSortTime(value: string | null | undefined) {
return Number.isNaN(parsed) ? 0 : parsed;
}
function getConversationLastMessageSortTime(item: ChatConversationSummary) {
const lastMessageTime = toConversationSortTime(item.lastMessageAt);
if (lastMessageTime > 0) {
return lastMessageTime;
}
return Math.max(
toConversationSortTime(item.createdAt),
toConversationSortTime(item.updatedAt),
);
}
export function sortChatConversationSummaries(items: ChatConversationSummary[]) {
return [...items].sort((left, right) => {
const leftTime = Math.max(
toConversationSortTime(left.lastMessageAt),
toConversationSortTime(left.updatedAt),
toConversationSortTime(left.createdAt),
);
const rightTime = Math.max(
toConversationSortTime(right.lastMessageAt),
toConversationSortTime(right.updatedAt),
toConversationSortTime(right.createdAt),
);
const leftTime = getConversationLastMessageSortTime(left);
const rightTime = getConversationLastMessageSortTime(right);
if (rightTime !== leftTime) {
return rightTime - leftTime;
@@ -289,18 +296,29 @@ function createLocalMessageId() {
return Date.now() * 1_000 + localMessageSequence;
}
function createRecoveredMessageId(requestId: string, variant: 'user' | 'codex' | 'activity') {
function createRecoveredMessageId(
requestId: string,
variant: 'user' | 'codex' | 'activity' | 'missing-request' | 'execution-failure',
) {
const baseId = hashRequestId(requestId) * 10;
if (variant === 'user') {
return -(baseId + 3);
}
if (variant === 'activity') {
if (variant === 'missing-request') {
return -(baseId + 2);
}
return -(baseId + 1);
if (variant === 'activity') {
return -(baseId + 1);
}
if (variant === 'execution-failure') {
return -(baseId + 4);
}
return -(baseId + 5);
}
function hashRequestId(value: string) {
@@ -355,6 +373,170 @@ function isActivityLogMessage(message: ChatMessage) {
return message.author === 'system' && message.text.startsWith(`${CHAT_ACTIVITY_MESSAGE_PREFIX}\n`);
}
export function isMissingRequestMessage(message: ChatMessage) {
return message.author === 'system' && message.text.startsWith(`${CHAT_MISSING_REQUEST_MESSAGE_PREFIX}\n`);
}
export function isExecutionFailureMessage(message: ChatMessage) {
return message.author === 'system' && message.text.startsWith(`${CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX}\n`);
}
function isEmptyCodexExecutionResponse(text: string) {
const normalized = text.replace(/\s+/g, ' ').trim();
return normalized === 'Codex 실행 결과가 비어 있습니다.';
}
function extractActivityLogFailureReason(lines?: string[] | null) {
const normalizedLines = (lines ?? []).map((line) => line.trim()).filter(Boolean);
for (let index = normalizedLines.length - 1; index >= 0; index -= 1) {
const line = normalizedLines[index];
if (!line.startsWith('# 오류:')) {
continue;
}
const raw = line.slice('# 오류:'.length).trim();
if (!raw) {
continue;
}
try {
const parsed = JSON.parse(raw) as { message?: unknown };
const message = typeof parsed.message === 'string' ? parsed.message.trim() : '';
if (message) {
return message;
}
} catch {
return raw;
}
}
return '';
}
function buildExecutionFailureMessage(reason: string) {
const normalizedReason = reason.trim();
if (!normalizedReason) {
return `${CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX}\n실행 중 오류가 발생했습니다.`;
}
const simplifiedReason = normalizedReason.includes("mkdir '") && normalizedReason.includes('/resource/uploads')
? `세션 리소스 업로드 폴더를 만들 권한이 없어 응답 생성이 중단되었습니다.\n\n원인: ${normalizedReason}`
: `실행 중 오류가 발생했습니다.\n\n원인: ${normalizedReason}`;
return `${CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX}\n${simplifiedReason}`;
}
function buildFailurePreviewText(reason: string) {
const normalizedReason = reason.trim();
if (!normalizedReason) {
return '실행 실패';
}
if (normalizedReason.includes("mkdir '") && normalizedReason.includes('/resource/uploads')) {
return '실행 실패: 세션 리소스 업로드 폴더 권한 오류';
}
return `실행 실패: ${normalizedReason}`;
}
function enrichFailedRequestsWithActivityLogs(
requests: ChatConversationRequest[],
activityLogs: ChatConversationActivityLog[],
) {
const activityLogMap = new Map(activityLogs.map((item) => [item.requestId.trim(), item]));
return requests.map((request) => {
if (request.status !== 'failed') {
return request;
}
const activityLog = activityLogMap.get(request.requestId.trim());
const failureReason = extractActivityLogFailureReason(activityLog?.lines);
const normalizedStatusMessage = String(request.statusMessage ?? '').trim();
if (!failureReason) {
return request;
}
if (normalizedStatusMessage && normalizedStatusMessage !== '요청 처리 실패') {
return request;
}
return {
...request,
statusMessage: failureReason,
};
});
}
function replaceGenericFailureMessages(
messages: ChatMessage[],
requests: ChatConversationRequest[],
activityLogs: ChatConversationActivityLog[],
): ChatMessage[] {
const requestMap = new Map(requests.map((item) => [item.requestId.trim(), item]));
const activityLogMap = new Map(activityLogs.map((item) => [item.requestId.trim(), item]));
return messages.map((message) => {
const requestId = message.clientRequestId?.trim() ?? '';
if (!requestId || message.author !== 'codex' || !isEmptyCodexExecutionResponse(message.text)) {
return message;
}
const request = requestMap.get(requestId);
if (request?.status !== 'failed') {
return message;
}
const failureReason = extractActivityLogFailureReason(activityLogMap.get(requestId)?.lines);
if (!failureReason) {
return message;
}
return {
...message,
author: 'system' as const,
text: buildExecutionFailureMessage(failureReason),
};
});
}
function resolveConversationFailurePreview(
currentPreview: string,
requests: ChatConversationRequest[],
activityLogs: ChatConversationActivityLog[],
) {
if (!isEmptyCodexExecutionResponse(currentPreview)) {
return currentPreview;
}
const latestFailedRequest = [...requests]
.reverse()
.find((request) => request.status === 'failed' && isEmptyCodexExecutionResponse(String(request.responseText ?? '').trim()));
if (!latestFailedRequest) {
return currentPreview;
}
const activityLog = activityLogs.find((item) => item.requestId.trim() === latestFailedRequest.requestId.trim());
const failureReason = extractActivityLogFailureReason(activityLog?.lines);
if (!failureReason) {
return currentPreview;
}
return buildFailurePreviewText(failureReason);
}
function extractActivityLogLines(text: string) {
return text
.slice(`${CHAT_ACTIVITY_MESSAGE_PREFIX}\n`.length)
@@ -934,7 +1116,9 @@ export async function fetchChatConversations() {
}
const clientId = getOrCreateClientId();
chatConversationListRequestPromise = requestChatApi<{ ok: boolean; items: ChatConversationSummary[] }>('/conversations')
chatConversationListRequestPromise = requestChatApi<{ ok: boolean; items: ChatConversationSummary[] }>(
'/conversations?limit=200',
)
.then((response) => {
return sortChatConversationSummaries(
response.items.map((item) => ({
@@ -971,17 +1155,24 @@ export async function fetchChatConversationDetail(
const response = await requestChatApi<ChatConversationDetailResponse>(
`/conversations/${encodeURIComponent(sessionId)}${query.toString() ? `?${query.toString()}` : ''}`,
);
const normalizedRequests = response.requests.map((item) => normalizeChatConversationRequest(item));
const normalizedRequests = enrichFailedRequestsWithActivityLogs(
response.requests.map((item) => normalizeChatConversationRequest(item)),
response.activityLogs,
);
const visibleRequestIds = new Set(
response.messages
.map((message) => message.clientRequestId?.trim() ?? '')
.filter(Boolean),
);
const hydratedMessages = hydrateActivityLogMessages(
response.messages,
replaceGenericFailureMessages(response.messages, normalizedRequests, response.activityLogs),
response.activityLogs.filter((item) => visibleRequestIds.has(item.requestId?.trim() ?? '')),
).filter(
(message) => message.author !== 'system' || isActivityLogMessage(message),
(message) =>
message.author !== 'system' ||
isActivityLogMessage(message) ||
isMissingRequestMessage(message) ||
isExecutionFailureMessage(message),
);
const recoveredMessages = buildRecoveredMessagesFromConversationDetail(normalizedRequests, response.activityLogs);
@@ -990,6 +1181,11 @@ export async function fetchChatConversationDetail(
messages: mergeRecoveredChatMessages(recoveredMessages, hydratedMessages),
item: {
...response.item,
lastMessagePreview: resolveConversationFailurePreview(
response.item.lastMessagePreview,
normalizedRequests,
response.activityLogs,
),
notifyOffline: resolveSyncedChatOfflineNotificationSetting(
response.item.sessionId,
response.item.notifyOffline,
@@ -1123,6 +1319,7 @@ export async function createChatConversationRoom(args: {
title?: string;
chatTypeId?: string | null;
lastChatTypeId?: string | null;
generalSectionName?: string | null;
contextLabel?: string;
contextDescription?: string;
notifyOffline?: boolean;
@@ -1136,6 +1333,7 @@ export async function createChatConversationRoom(args: {
title: args.title ?? '새 대화',
chatTypeId: args.chatTypeId ?? null,
lastChatTypeId: args.lastChatTypeId ?? args.chatTypeId ?? null,
generalSectionName: args.generalSectionName ?? null,
contextLabel: args.contextLabel ?? null,
contextDescription: args.contextDescription ?? null,
notifyOffline,
@@ -1173,6 +1371,7 @@ export async function updateChatConversationRoom(
title?: string;
chatTypeId?: string | null;
lastChatTypeId?: string | null;
generalSectionName?: string | null;
contextLabel?: string | null;
contextDescription?: string | null;
notifyOffline?: boolean;
@@ -1307,6 +1506,14 @@ function isSameChatMessage(left: ChatMessage, right: ChatMessage) {
return Boolean(left.clientRequestId && right.clientRequestId && left.clientRequestId === right.clientRequestId);
}
if (isMissingRequestMessage(left) && isMissingRequestMessage(right)) {
return Boolean(left.clientRequestId && right.clientRequestId && left.clientRequestId === right.clientRequestId);
}
if (isExecutionFailureMessage(left) && isExecutionFailureMessage(right)) {
return Boolean(left.clientRequestId && right.clientRequestId && left.clientRequestId === right.clientRequestId);
}
return Boolean(
(left.author === 'user' || left.author === 'codex') &&
left.author === right.author &&
@@ -1321,6 +1528,14 @@ function buildComparableChatMessageKey(message: ChatMessage) {
return `activity:${message.clientRequestId}`;
}
if (isMissingRequestMessage(message) && message.clientRequestId) {
return `missing-request:${message.clientRequestId}`;
}
if (isExecutionFailureMessage(message) && message.clientRequestId) {
return `execution-failure:${message.clientRequestId}`;
}
if (message.author === 'user' && message.clientRequestId) {
return `user-request:${message.clientRequestId}`;
}
@@ -1337,6 +1552,123 @@ function getComparableChatMessageTime(message: ChatMessage) {
return Number.isFinite(parsed) ? parsed : 0;
}
function getChatMessageRequestId(message: ChatMessage) {
return message.clientRequestId?.trim() || '';
}
function getChatMessageOrderRank(message: ChatMessage) {
if (message.author === 'user') {
return 0;
}
if (isMissingRequestMessage(message)) {
return 1;
}
if (isExecutionFailureMessage(message)) {
return 2;
}
if (isActivityLogMessage(message)) {
return 3;
}
if (message.author === 'codex') {
return 3;
}
return 4;
}
function sortConversationMessages(messages: ChatMessage[]) {
if (messages.length <= 1) {
return messages;
}
const messageIndexMap = new Map(messages.map((message, index) => [message, index]));
const requestOrder = new Map<
string,
{
time: number;
firstIndex: number;
}
>();
messages.forEach((message, index) => {
const requestId = getChatMessageRequestId(message);
if (!requestId) {
return;
}
const time = getComparableChatMessageTime(message);
const existing = requestOrder.get(requestId);
if (!existing) {
requestOrder.set(requestId, {
time,
firstIndex: index,
});
return;
}
requestOrder.set(requestId, {
time:
existing.time > 0 && time > 0
? Math.min(existing.time, time)
: existing.time > 0
? existing.time
: time,
firstIndex: Math.min(existing.firstIndex, index),
});
});
return [...messages].sort((left, right) => {
const leftRequestId = getChatMessageRequestId(left);
const rightRequestId = getChatMessageRequestId(right);
if (leftRequestId && rightRequestId && leftRequestId === rightRequestId) {
const rankDiff = getChatMessageOrderRank(left) - getChatMessageOrderRank(right);
if (rankDiff !== 0) {
return rankDiff;
}
}
const leftOrder = leftRequestId ? requestOrder.get(leftRequestId) : null;
const rightOrder = rightRequestId ? requestOrder.get(rightRequestId) : null;
const leftTime = leftOrder?.time ?? getComparableChatMessageTime(left);
const rightTime = rightOrder?.time ?? getComparableChatMessageTime(right);
if (leftTime !== rightTime) {
return leftTime - rightTime;
}
const leftIndex = leftOrder?.firstIndex ?? messageIndexMap.get(left) ?? 0;
const rightIndex = rightOrder?.firstIndex ?? messageIndexMap.get(right) ?? 0;
if (leftIndex !== rightIndex) {
return leftIndex - rightIndex;
}
if (leftRequestId && rightRequestId && leftRequestId !== rightRequestId) {
const requestDiff = leftRequestId.localeCompare(rightRequestId, 'ko-KR');
if (requestDiff !== 0) {
return requestDiff;
}
}
const messageTimeDiff = getComparableChatMessageTime(left) - getComparableChatMessageTime(right);
if (messageTimeDiff !== 0) {
return messageTimeDiff;
}
return left.id - right.id;
});
}
function buildRecoveredMessagesFromConversationDetail(
requests: ChatConversationRequest[],
activityLogs: ChatConversationActivityLog[],
@@ -1354,6 +1686,9 @@ function buildRecoveredMessagesFromConversationDetail(
const userText = String(request.userText ?? '').trim();
const responseText = String(request.responseText ?? '').trim();
const activityLog = activityLogMap.get(requestId);
const failureReason = extractActivityLogFailureReason(activityLog?.lines);
const shouldReplaceEmptyFailureResponse =
request.status === 'failed' && isEmptyCodexExecutionResponse(responseText) && Boolean(failureReason);
if (userText) {
nextMessages.push({
@@ -1363,9 +1698,17 @@ function buildRecoveredMessagesFromConversationDetail(
timestamp: request.createdAt || request.updatedAt || '',
clientRequestId: requestId,
});
} else if (responseText || activityLog?.lines.length) {
nextMessages.push({
id: createRecoveredMessageId(requestId, 'missing-request'),
author: 'system',
text: `${CHAT_MISSING_REQUEST_MESSAGE_PREFIX}\n이 요청은 저장된 원문이 없어 실제 요청 문장을 표시할 수 없습니다.`,
timestamp: request.createdAt || request.updatedAt || '',
clientRequestId: requestId,
});
}
if (responseText) {
if (responseText && !shouldReplaceEmptyFailureResponse) {
nextMessages.push({
id: request.responseMessageId ?? createRecoveredMessageId(requestId, 'codex'),
author: 'codex',
@@ -1375,6 +1718,16 @@ function buildRecoveredMessagesFromConversationDetail(
});
}
if (shouldReplaceEmptyFailureResponse) {
nextMessages.push({
id: request.responseMessageId ?? createRecoveredMessageId(requestId, 'execution-failure'),
author: 'system',
text: buildExecutionFailureMessage(failureReason),
timestamp: request.answeredAt || request.updatedAt || request.createdAt || '',
clientRequestId: requestId,
});
}
if (activityLog && activityLog.lines.length > 0) {
nextMessages.push({
id: createRecoveredMessageId(requestId, 'activity'),
@@ -1386,15 +1739,7 @@ function buildRecoveredMessagesFromConversationDetail(
}
});
return nextMessages.sort((left, right) => {
const timeDiff = getComparableChatMessageTime(left) - getComparableChatMessageTime(right);
if (timeDiff !== 0) {
return timeDiff;
}
return left.id - right.id;
});
return sortConversationMessages(nextMessages);
}
export function mergeRecoveredChatMessages(previous: ChatMessage[], incoming: ChatMessage[]) {
@@ -1459,7 +1804,7 @@ export function mergeRecoveredChatMessages(previous: ChatMessage[], incoming: Ch
});
const unmatchedLocalMessages = Array.from(previousBuckets.values()).flat();
const nextMessages = [...mergedServerMessages, ...unmatchedLocalMessages];
const nextMessages = sortConversationMessages([...mergedServerMessages, ...unmatchedLocalMessages]);
return areChatMessagesEquivalent(previous, nextMessages) ? previous : nextMessages;
}

View File

@@ -0,0 +1,49 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.useErrorLogs = exports.useChatConnection = exports.subscribeChatConnection = exports.setSharedChatRuntimeSnapshot = exports.getSharedChatRuntimeSnapshot = exports.updateChatConversationRoom = exports.upsertChatMessage = exports.uploadChatComposerFile = exports.sortChatConversationSummaries = exports.setChatClientSessionId = exports.setStoredChatSessionLastTypeId = exports.resetLastReceivedChatEventId = exports.removeChatRuntimeJob = exports.renameChatConversationRoom = exports.mergeRecoveredChatMessages = exports.markChatConversationResponsesRead = exports.getChatClientSessionId = exports.isPreparingChatReplyText = exports.isMissingRequestMessage = exports.getStoredChatSessionLastTypeId = exports.fetchChatRuntimeSnapshot = exports.fetchChatRuntimeJobDetail = exports.fetchChatConversations = exports.fetchChatConversationDetail = exports.deleteChatConversationRoom = exports.deleteChatConversationRequest = exports.cancelChatRuntimeJob = exports.createLocalMessage = exports.createIntroMessage = exports.createChatMessage = exports.createChatConversationRoom = exports.createActivityLogPlaceholder = exports.resolvePreviewBodyForCopy = exports.copyText = exports.copyPreviewContent = exports.clearStoredChatClientConversationState = exports.buildOfflineReply = exports.ErrorLogViewer = exports.ChatRuntimeDashboard = exports.ChatConversationView = void 0;
var ChatConversationView_1 = require("./ChatConversationView");
Object.defineProperty(exports, "ChatConversationView", { enumerable: true, get: function () { return ChatConversationView_1.ChatConversationView; } });
var ChatRuntimeDashboard_1 = require("./ChatRuntimeDashboard");
Object.defineProperty(exports, "ChatRuntimeDashboard", { enumerable: true, get: function () { return ChatRuntimeDashboard_1.ChatRuntimeDashboard; } });
var ErrorLogViewer_1 = require("./ErrorLogViewer");
Object.defineProperty(exports, "ErrorLogViewer", { enumerable: true, get: function () { return ErrorLogViewer_1.ErrorLogViewer; } });
var chatUtils_1 = require("./chatUtils");
Object.defineProperty(exports, "buildOfflineReply", { enumerable: true, get: function () { return chatUtils_1.buildOfflineReply; } });
Object.defineProperty(exports, "clearStoredChatClientConversationState", { enumerable: true, get: function () { return chatUtils_1.clearStoredChatClientConversationState; } });
Object.defineProperty(exports, "copyPreviewContent", { enumerable: true, get: function () { return chatUtils_1.copyPreviewContent; } });
Object.defineProperty(exports, "copyText", { enumerable: true, get: function () { return chatUtils_1.copyText; } });
Object.defineProperty(exports, "resolvePreviewBodyForCopy", { enumerable: true, get: function () { return chatUtils_1.resolvePreviewBodyForCopy; } });
Object.defineProperty(exports, "createActivityLogPlaceholder", { enumerable: true, get: function () { return chatUtils_1.createActivityLogPlaceholder; } });
Object.defineProperty(exports, "createChatConversationRoom", { enumerable: true, get: function () { return chatUtils_1.createChatConversationRoom; } });
Object.defineProperty(exports, "createChatMessage", { enumerable: true, get: function () { return chatUtils_1.createChatMessage; } });
Object.defineProperty(exports, "createIntroMessage", { enumerable: true, get: function () { return chatUtils_1.createIntroMessage; } });
Object.defineProperty(exports, "createLocalMessage", { enumerable: true, get: function () { return chatUtils_1.createLocalMessage; } });
Object.defineProperty(exports, "cancelChatRuntimeJob", { enumerable: true, get: function () { return chatUtils_1.cancelChatRuntimeJob; } });
Object.defineProperty(exports, "deleteChatConversationRequest", { enumerable: true, get: function () { return chatUtils_1.deleteChatConversationRequest; } });
Object.defineProperty(exports, "deleteChatConversationRoom", { enumerable: true, get: function () { return chatUtils_1.deleteChatConversationRoom; } });
Object.defineProperty(exports, "fetchChatConversationDetail", { enumerable: true, get: function () { return chatUtils_1.fetchChatConversationDetail; } });
Object.defineProperty(exports, "fetchChatConversations", { enumerable: true, get: function () { return chatUtils_1.fetchChatConversations; } });
Object.defineProperty(exports, "fetchChatRuntimeJobDetail", { enumerable: true, get: function () { return chatUtils_1.fetchChatRuntimeJobDetail; } });
Object.defineProperty(exports, "fetchChatRuntimeSnapshot", { enumerable: true, get: function () { return chatUtils_1.fetchChatRuntimeSnapshot; } });
Object.defineProperty(exports, "getStoredChatSessionLastTypeId", { enumerable: true, get: function () { return chatUtils_1.getStoredChatSessionLastTypeId; } });
Object.defineProperty(exports, "isMissingRequestMessage", { enumerable: true, get: function () { return chatUtils_1.isMissingRequestMessage; } });
Object.defineProperty(exports, "isPreparingChatReplyText", { enumerable: true, get: function () { return chatUtils_1.isPreparingChatReplyText; } });
Object.defineProperty(exports, "getChatClientSessionId", { enumerable: true, get: function () { return chatUtils_1.getChatClientSessionId; } });
Object.defineProperty(exports, "markChatConversationResponsesRead", { enumerable: true, get: function () { return chatUtils_1.markChatConversationResponsesRead; } });
Object.defineProperty(exports, "mergeRecoveredChatMessages", { enumerable: true, get: function () { return chatUtils_1.mergeRecoveredChatMessages; } });
Object.defineProperty(exports, "renameChatConversationRoom", { enumerable: true, get: function () { return chatUtils_1.renameChatConversationRoom; } });
Object.defineProperty(exports, "removeChatRuntimeJob", { enumerable: true, get: function () { return chatUtils_1.removeChatRuntimeJob; } });
Object.defineProperty(exports, "resetLastReceivedChatEventId", { enumerable: true, get: function () { return chatUtils_1.resetLastReceivedChatEventId; } });
Object.defineProperty(exports, "setStoredChatSessionLastTypeId", { enumerable: true, get: function () { return chatUtils_1.setStoredChatSessionLastTypeId; } });
Object.defineProperty(exports, "setChatClientSessionId", { enumerable: true, get: function () { return chatUtils_1.setChatClientSessionId; } });
Object.defineProperty(exports, "sortChatConversationSummaries", { enumerable: true, get: function () { return chatUtils_1.sortChatConversationSummaries; } });
Object.defineProperty(exports, "uploadChatComposerFile", { enumerable: true, get: function () { return chatUtils_1.uploadChatComposerFile; } });
Object.defineProperty(exports, "upsertChatMessage", { enumerable: true, get: function () { return chatUtils_1.upsertChatMessage; } });
Object.defineProperty(exports, "updateChatConversationRoom", { enumerable: true, get: function () { return chatUtils_1.updateChatConversationRoom; } });
var useChatConnection_1 = require("./useChatConnection");
Object.defineProperty(exports, "getSharedChatRuntimeSnapshot", { enumerable: true, get: function () { return useChatConnection_1.getSharedChatRuntimeSnapshot; } });
Object.defineProperty(exports, "setSharedChatRuntimeSnapshot", { enumerable: true, get: function () { return useChatConnection_1.setSharedChatRuntimeSnapshot; } });
Object.defineProperty(exports, "subscribeChatConnection", { enumerable: true, get: function () { return useChatConnection_1.subscribeChatConnection; } });
Object.defineProperty(exports, "useChatConnection", { enumerable: true, get: function () { return useChatConnection_1.useChatConnection; } });
var useErrorLogs_1 = require("./useErrorLogs");
Object.defineProperty(exports, "useErrorLogs", { enumerable: true, get: function () { return useErrorLogs_1.useErrorLogs; } });

View File

@@ -20,6 +20,7 @@ export {
fetchChatRuntimeJobDetail,
fetchChatRuntimeSnapshot,
getStoredChatSessionLastTypeId,
isMissingRequestMessage,
isPreparingChatReplyText,
getChatClientSessionId,
markChatConversationResponsesRead,

View File

@@ -1,11 +1,32 @@
const AUTO_DETECTED_PREVIEW_URL_PATTERN = /(https?:\/\/[^\s<>)\]]+|\/[A-Za-z0-9._~:/?#[\]@!$&'*+,;=%-]+)/g;
const AUTO_DETECTED_PREVIEW_URL_PATTERN =
/(https?:\/\/[^\s<>)\]]+|\/(?:[A-Za-z0-9._~%-][A-Za-z0-9._~:/?#[\]@!$&'*+,;=%-]*))/g;
const LOCAL_RESOURCE_PREFIXES = ['/api/chat/resources/', '/public/.codex_chat/', '/.codex_chat/'] as const;
const PREVIEWABLE_FILE_EXTENSION_PATTERN =
/\.(png|jpe?g|gif|webp|svg|bmp|ico|mp4|webm|mov|m4v|ogg|md|markdown|diff|patch|ts|tsx|js|jsx|json|css|scss|html|xml|java|kt|sql|sh|py|go|rs|c|cpp|h|hpp|yml|yaml|txt|log|csv|pdf)$/i;
function stripCodeFenceBlocks(text: string) {
return String(text ?? '').replace(/```[\s\S]*?```/g, '');
}
function trimAutoDetectedUrl(value: string) {
return String(value ?? '').trim().replace(/[`\])}>.,;!?]+$/g, '');
}
function isLikelyLocalPreviewUrl(value: string) {
if (LOCAL_RESOURCE_PREFIXES.some((prefix) => value.startsWith(prefix))) {
return true;
}
const pathname = value.split(/[?#]/, 1)[0] ?? '';
return PREVIEWABLE_FILE_EXTENSION_PATTERN.test(pathname);
}
export function extractAutoDetectedPreviewUrls(text: string) {
const normalized = String(text ?? '');
const normalized = stripCodeFenceBlocks(text);
const urls: string[] = [];
for (const match of normalized.matchAll(AUTO_DETECTED_PREVIEW_URL_PATTERN)) {
const value = match[0]?.trim();
const value = trimAutoDetectedUrl(match[0] ?? '');
if (!value) {
continue;
@@ -19,6 +40,10 @@ export function extractAutoDetectedPreviewUrls(text: string) {
continue;
}
if (value.startsWith('/') && !isLikelyLocalPreviewUrl(value)) {
continue;
}
urls.push(value);
}

View File

@@ -16,6 +16,11 @@ function normalizeUrl(value: string) {
return '';
}
const malformedResourceMatch = normalized.match(/^https?:\/(api\/chat\/resources\/.+)$/i);
if (malformedResourceMatch?.[1]) {
return `/${malformedResourceMatch[1]}`;
}
if (/^(?:https?:\/\/|\/)/i.test(normalized)) {
return normalized;
}
@@ -23,11 +28,43 @@ function normalizeUrl(value: string) {
return '';
}
function decodeUrlComponentSafely(value: string) {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function resolveLinkCardUrlAndActionLabel(rawUrl: string, rawActionLabel?: string) {
let resolvedUrl = normalizeText(rawUrl);
let resolvedActionLabel = normalizeText(rawActionLabel);
if (!resolvedActionLabel) {
const decodedUrl = decodeUrlComponentSafely(resolvedUrl);
const dividerIndex = decodedUrl.lastIndexOf('|');
if (dividerIndex > 0 && dividerIndex < decodedUrl.length - 1) {
resolvedUrl = decodedUrl.slice(0, dividerIndex).trim();
resolvedActionLabel = decodedUrl.slice(dividerIndex + 1).trim();
}
}
return {
url: normalizeUrl(resolvedUrl),
actionLabel: resolvedActionLabel || null,
};
}
function hasKnownFileExtension(url: string) {
const pathname = url.split('?')[0] ?? '';
return /\.[a-z0-9]{1,8}$/i.test(pathname);
}
function isInternalResourceUrl(url: string) {
return RESOURCE_PATH_PREFIXES.some((prefix) => url.startsWith(prefix));
}
function isStructuredLinkCardCandidate(url: string) {
const normalized = normalizeUrl(url);
@@ -35,15 +72,11 @@ function isStructuredLinkCardCandidate(url: string) {
return false;
}
if (RESOURCE_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
if (isInternalResourceUrl(normalized)) {
return false;
}
if (/^https?:\/\//i.test(normalized)) {
return !hasKnownFileExtension(normalized);
}
return !hasKnownFileExtension(normalized);
return /^https?:\/\//i.test(normalized) && !hasKnownFileExtension(normalized);
}
function buildFallbackLinkTitle(url: string) {
@@ -88,8 +121,7 @@ function buildLinkCardPart(rawBody: string): ChatMessagePart | null {
const [rawTitle, rawUrl, rawActionLabel] = segments;
const title = normalizeText(rawTitle);
const url = normalizeUrl(rawUrl);
const actionLabel = normalizeText(rawActionLabel) || null;
const { url, actionLabel } = resolveLinkCardUrlAndActionLabel(rawUrl, rawActionLabel);
if (!title || !url) {
return null;
@@ -154,6 +186,14 @@ export function extractChatMessageParts(text: string) {
if (!pushPart(buildLinkCardPart(matched[1] ?? ''))) {
keptLines.push(line);
continue;
}
const latestPart = parts.at(-1);
if (latestPart && isInternalResourceUrl(latestPart.url)) {
parts.pop();
seenLinkKeys.delete(`${latestPart.type}:${latestPart.title}:${latestPart.url}:${latestPart.actionLabel ?? ''}`);
keptLines.push(latestPart.url);
}
}

View File

@@ -14,10 +14,117 @@ export type PreviewItem = {
source: 'message' | 'context';
};
const CHAT_RESOURCE_INTERNAL_SEGMENTS = new Set(['resource', 'uploads', 'source', 'src']);
const CHAT_RESOURCE_HIDDEN_FILE_NAMES = new Set(['.env']);
const CHAT_API_RESOURCE_MARKER = '/api/chat/resources/';
const RESOURCE_STRIP_ALLOWED_KINDS = new Set<PreviewKind>([
'image',
'video',
'markdown',
'code',
'diff',
'document',
'pdf',
'file',
]);
function normalizePreviewUrl(value: string) {
return normalizeChatResourceUrl(value);
}
function parsePreviewUrl(url: string) {
try {
return new URL(url, typeof window !== 'undefined' ? window.location.origin : 'https://local.invalid');
} catch {
return null;
}
}
function extractInternalChatResourcePath(pathname: string) {
const normalizedPathname = String(pathname ?? '').trim();
if (!normalizedPathname) {
return '';
}
if (normalizedPathname.includes('/.codex_chat/')) {
const markerIndex = normalizedPathname.lastIndexOf('/.codex_chat/');
return normalizedPathname.slice(markerIndex + 1);
}
const apiMarkerIndex = normalizedPathname.lastIndexOf(CHAT_API_RESOURCE_MARKER);
if (apiMarkerIndex >= 0) {
return normalizedPathname.slice(apiMarkerIndex + CHAT_API_RESOURCE_MARKER.length).replace(/^\/+/, '');
}
return '';
}
function hasVisibleFileExtension(fileName: string) {
return /\.[a-z0-9]{1,16}$/i.test(fileName);
}
function hasSupportedPreviewFileExtension(fileName: string) {
return /\.(png|jpe?g|gif|webp|svg|bmp|ico|mp4|webm|mov|m4v|ogg|md|markdown|diff|patch|ts|tsx|js|jsx|json|css|scss|html|xml|java|kt|sql|sh|py|go|rs|c|cpp|h|hpp|yml|yaml|txt|log|csv|pdf)$/i.test(
fileName,
);
}
function isMarkdownResourceFile(fileName: string) {
return /\.(md|markdown)$/i.test(fileName);
}
function shouldHideInternalChatResource(url: string) {
const parsed = parsePreviewUrl(url);
const pathname = parsed?.pathname ?? '';
const internalResourcePath = extractInternalChatResourcePath(pathname);
const normalizedInternalResourcePath = internalResourcePath.toLowerCase();
if (!normalizedInternalResourcePath.startsWith('.codex_chat/')) {
return false;
}
const segments = internalResourcePath.split('/').filter(Boolean);
const lastSegment = segments.at(-1)?.trim() ?? '';
const normalizedLastSegment = lastSegment.toLowerCase();
const resourceSegmentIndex = segments.findIndex((segment) => segment === 'resource');
const nextSegment = resourceSegmentIndex >= 0 ? segments[resourceSegmentIndex + 1]?.toLowerCase() ?? '' : '';
if (!lastSegment || pathname.endsWith('/')) {
return true;
}
if (CHAT_RESOURCE_INTERNAL_SEGMENTS.has(normalizedLastSegment)) {
return true;
}
if (!hasVisibleFileExtension(lastSegment)) {
return true;
}
if (CHAT_RESOURCE_INTERNAL_SEGMENTS.has(nextSegment) && !hasVisibleFileExtension(lastSegment)) {
return true;
}
if (normalizedLastSegment.startsWith('.')) {
return true;
}
if (CHAT_RESOURCE_HIDDEN_FILE_NAMES.has(normalizedLastSegment)) {
return true;
}
if (resourceSegmentIndex >= 0 && nextSegment === 'src' && !isMarkdownResourceFile(lastSegment)) {
return true;
}
if (!hasSupportedPreviewFileExtension(lastSegment)) {
return true;
}
return false;
}
function isPreviewRouteUrl(url: string) {
if (typeof window === 'undefined') {
return false;
@@ -118,15 +225,23 @@ export function extractPreviewItems(messages: ChatMessage[]) {
)
.map((part) => part.url);
const matches = [
...extractAutoDetectedPreviewUrls(message.text),
...extractHiddenPreviewUrls(message.text),
...structuredLinkUrls,
...extractAutoDetectedPreviewUrls(message.text),
];
matches.forEach((matchedUrl) => {
const normalizedUrl = normalizePreviewUrl(matchedUrl);
const kind = classifyPreviewKind(normalizedUrl);
if (shouldHideInternalChatResource(normalizedUrl)) {
return;
}
if (!RESOURCE_STRIP_ALLOWED_KINDS.has(kind)) {
return;
}
if (seen.has(normalizedUrl)) {
return;
}

View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -44,9 +44,11 @@ export type ChatViewContext = {
export type ChatConversationSummary = {
sessionId: string;
clientId: string | null;
isDraftOnly?: boolean;
title: string;
chatTypeId: string | null;
lastChatTypeId: string | null;
generalSectionName: string | null;
contextLabel: string | null;
contextDescription: string | null;
notifyOffline: boolean;
@@ -56,7 +58,9 @@ export type ChatConversationSummary = {
currentJobMessage: string | null;
currentQueueSize: number;
currentStatusUpdatedAt: string | null;
lastRequestPreview: string;
lastMessagePreview: string;
lastResponsePreview: string;
createdAt: string;
updatedAt: string;
lastMessageAt: string | null;

View File

@@ -0,0 +1,495 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getChatConnectionSnapshot = getChatConnectionSnapshot;
exports.subscribeChatConnection = subscribeChatConnection;
exports.getSharedChatRuntimeSnapshot = getSharedChatRuntimeSnapshot;
exports.setSharedChatRuntimeSnapshot = setSharedChatRuntimeSnapshot;
exports.useChatConnection = useChatConnection;
var react_1 = require("react");
var chatUtils_1 = require("./chatUtils");
var tokenAccess_1 = require("../tokenAccess");
var DISCONNECT_UI_DELAY_MS = 1500;
var PRESENCE_PING_INTERVAL_MS = 20000;
var sharedChatConnection = {
connectionState: 'connecting',
connectionErrorDetail: '',
runtimeSnapshot: null,
socketRef: { current: null },
reconnectTimerId: null,
disconnectUiTimerId: null,
connectTimeoutId: null,
sessionId: '',
currentContext: null,
setMessages: null,
onMessageEvent: undefined,
onJobEvent: undefined,
onRuntimeEvent: undefined,
onRuntimeDetailEvent: undefined,
onActivityEvent: undefined,
lastEventId: 0,
websocketUrl: '',
subscribers: new Set(),
pingSubscriberCount: 0,
consumerCount: 0,
pingIntervalId: null,
visibilityHandlerInstalled: false,
pageShowHandlerInstalled: false,
focusHandlerInstalled: false,
onlineHandlerInstalled: false,
hasConnectedOnce: false,
suppressDisconnectNotification: false,
lastBackgroundAt: null,
};
function emitSharedState() {
sharedChatConnection.subscribers.forEach(function (listener) {
listener();
});
}
function getSnapshot() {
return {
connectionState: sharedChatConnection.connectionState,
connectionErrorDetail: sharedChatConnection.connectionErrorDetail,
runtimeSnapshot: sharedChatConnection.runtimeSnapshot,
};
}
function getChatConnectionSnapshot() {
return getSnapshot();
}
function subscribeChatConnection(listener) {
sharedChatConnection.subscribers.add(listener);
return function () {
sharedChatConnection.subscribers.delete(listener);
};
}
function getSharedChatRuntimeSnapshot() {
return sharedChatConnection.runtimeSnapshot;
}
function setSharedChatRuntimeSnapshot(snapshot) {
if (sharedChatConnection.runtimeSnapshot === snapshot) {
return;
}
sharedChatConnection.runtimeSnapshot = snapshot;
emitSharedState();
}
function setSharedConnectionState(nextState) {
if (sharedChatConnection.connectionState === nextState) {
return;
}
sharedChatConnection.connectionState = nextState;
emitSharedState();
}
function setSharedConnectionError(detail) {
if (sharedChatConnection.connectionErrorDetail === detail) {
return;
}
sharedChatConnection.connectionErrorDetail = detail;
emitSharedState();
}
function clearReconnectTimer() {
if (sharedChatConnection.reconnectTimerId !== null) {
window.clearTimeout(sharedChatConnection.reconnectTimerId);
sharedChatConnection.reconnectTimerId = null;
}
}
function clearDisconnectUiTimer() {
if (sharedChatConnection.disconnectUiTimerId !== null) {
window.clearTimeout(sharedChatConnection.disconnectUiTimerId);
sharedChatConnection.disconnectUiTimerId = null;
}
}
function clearConnectTimeout() {
if (sharedChatConnection.connectTimeoutId !== null) {
window.clearTimeout(sharedChatConnection.connectTimeoutId);
sharedChatConnection.connectTimeoutId = null;
}
}
function sendContextUpdate(context) {
if (context === void 0) { context = sharedChatConnection.currentContext; }
var socket = sharedChatConnection.socketRef.current;
if (!socket || socket.readyState !== WebSocket.OPEN || !context) {
return;
}
var liveVisibilityState = typeof document !== 'undefined' && document.visibilityState === 'hidden' ? 'hidden' : 'visible';
var livePageUrl = typeof window !== 'undefined' ? window.location.href : context.pageUrl;
socket.send(JSON.stringify({
type: 'context:update',
payload: {
pageId: context.pageId,
pageTitle: context.pageTitle,
topMenu: context.topMenu,
focusedComponentId: context.focusedComponentId,
pageUrl: livePageUrl,
isStandaloneMode: context.isStandaloneMode,
pageVisibilityState: liveVisibilityState,
chatTypeId: context.chatTypeId,
chatTypeLabel: context.chatTypeLabel,
chatTypeDescription: context.chatTypeDescription,
},
}));
}
function sendPresencePing() {
var socket = sharedChatConnection.socketRef.current;
if (!socket || socket.readyState !== WebSocket.OPEN) {
return;
}
socket.send(JSON.stringify({
type: 'presence:ping',
payload: {
at: Date.now(),
},
}));
}
function ensureSharedSocket() {
var socket = sharedChatConnection.socketRef.current;
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
return;
}
connectSharedSocket();
}
function sendEventReceived(eventId) {
var socket = sharedChatConnection.socketRef.current;
if (!socket || socket.readyState !== WebSocket.OPEN || !Number.isFinite(eventId) || eventId <= 0) {
return;
}
socket.send(JSON.stringify({
type: 'event:received',
payload: {
eventId: eventId,
},
}));
}
function stopPresenceMonitoring() {
if (sharedChatConnection.pingIntervalId !== null) {
window.clearInterval(sharedChatConnection.pingIntervalId);
sharedChatConnection.pingIntervalId = null;
}
if (sharedChatConnection.visibilityHandlerInstalled) {
window.removeEventListener('visibilitychange', handleVisibilityChange);
sharedChatConnection.visibilityHandlerInstalled = false;
}
if (sharedChatConnection.pageShowHandlerInstalled) {
window.removeEventListener('pageshow', handlePageShow);
sharedChatConnection.pageShowHandlerInstalled = false;
}
if (sharedChatConnection.focusHandlerInstalled) {
window.removeEventListener('focus', handleWindowFocus);
sharedChatConnection.focusHandlerInstalled = false;
}
if (sharedChatConnection.onlineHandlerInstalled) {
window.removeEventListener('online', handleWindowOnline);
sharedChatConnection.onlineHandlerInstalled = false;
}
}
function handleVisibilityChange() {
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {
sharedChatConnection.lastBackgroundAt = Date.now();
sendContextUpdate(sharedChatConnection.currentContext);
sendPresencePing();
return;
}
ensureSharedSocket();
sendPresencePing();
sendContextUpdate(sharedChatConnection.currentContext);
}
function handlePageShow() {
ensureSharedSocket();
sendPresencePing();
sendContextUpdate(sharedChatConnection.currentContext);
}
function handleWindowFocus() {
ensureSharedSocket();
sendPresencePing();
}
function handleWindowOnline() {
ensureSharedSocket();
}
function startPresenceMonitoring() {
if (sharedChatConnection.pingSubscriberCount <= 0 || sharedChatConnection.connectionState !== 'connected') {
stopPresenceMonitoring();
return;
}
sharedChatConnection.lastBackgroundAt = null;
sendPresencePing();
if (sharedChatConnection.pingIntervalId === null) {
sharedChatConnection.pingIntervalId = window.setInterval(function () {
sendPresencePing();
}, PRESENCE_PING_INTERVAL_MS);
}
if (!sharedChatConnection.visibilityHandlerInstalled) {
window.addEventListener('visibilitychange', handleVisibilityChange);
sharedChatConnection.visibilityHandlerInstalled = true;
}
if (!sharedChatConnection.pageShowHandlerInstalled) {
window.addEventListener('pageshow', handlePageShow);
sharedChatConnection.pageShowHandlerInstalled = true;
}
if (!sharedChatConnection.focusHandlerInstalled) {
window.addEventListener('focus', handleWindowFocus);
sharedChatConnection.focusHandlerInstalled = true;
}
if (!sharedChatConnection.onlineHandlerInstalled) {
window.addEventListener('online', handleWindowOnline);
sharedChatConnection.onlineHandlerInstalled = true;
}
}
function scheduleReconnect() {
if (sharedChatConnection.reconnectTimerId !== null || !sharedChatConnection.sessionId) {
return;
}
sharedChatConnection.reconnectTimerId = window.setTimeout(function () {
sharedChatConnection.reconnectTimerId = null;
connectSharedSocket();
}, chatUtils_1.CHAT_CONNECTION.reconnectDelayMs);
}
function handleSharedDisconnect(message, detail) {
setSharedConnectionError(detail !== null && detail !== void 0 ? detail : '');
clearDisconnectUiTimer();
if (sharedChatConnection.connectionState !== 'connected') {
setSharedConnectionState('disconnected');
}
else {
sharedChatConnection.disconnectUiTimerId = window.setTimeout(function () {
var _a;
sharedChatConnection.disconnectUiTimerId = null;
if (((_a = sharedChatConnection.socketRef.current) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
return;
}
setSharedConnectionState('disconnected');
}, DISCONNECT_UI_DELAY_MS);
}
if (message) {
scheduleReconnect();
}
}
function disconnectSharedSocket() {
clearReconnectTimer();
clearConnectTimeout();
clearDisconnectUiTimer();
stopPresenceMonitoring();
var socket = sharedChatConnection.socketRef.current;
sharedChatConnection.suppressDisconnectNotification = true;
sharedChatConnection.socketRef.current = null;
socket === null || socket === void 0 ? void 0 : socket.close();
}
function releaseSharedConnectionConsumer() {
sharedChatConnection.consumerCount = Math.max(0, sharedChatConnection.consumerCount - 1);
if (sharedChatConnection.consumerCount > 0) {
return;
}
sharedChatConnection.currentContext = null;
sharedChatConnection.setMessages = null;
sharedChatConnection.onMessageEvent = undefined;
sharedChatConnection.onJobEvent = undefined;
sharedChatConnection.onRuntimeEvent = undefined;
sharedChatConnection.onRuntimeDetailEvent = undefined;
setSharedChatRuntimeSnapshot(null);
disconnectSharedSocket();
setSharedConnectionError('');
setSharedConnectionState('disconnected');
}
function connectSharedSocket() {
if (!sharedChatConnection.sessionId || !sharedChatConnection.setMessages) {
return;
}
if (!(0, tokenAccess_1.hasRegisteredAccessTokenAccess)()) {
clearReconnectTimer();
clearConnectTimeout();
clearDisconnectUiTimer();
stopPresenceMonitoring();
setSharedConnectionError('등록된 접근 토큰이 없어 채팅 연결을 시작하지 않았습니다.');
setSharedConnectionState('disconnected');
return;
}
var currentSocket = sharedChatConnection.socketRef.current;
if (currentSocket && (currentSocket.readyState === WebSocket.OPEN || currentSocket.readyState === WebSocket.CONNECTING)) {
return;
}
clearReconnectTimer();
clearConnectTimeout();
clearDisconnectUiTimer();
if (sharedChatConnection.connectionState !== 'connected') {
setSharedConnectionState('connecting');
}
sharedChatConnection.websocketUrl = (0, chatUtils_1.resolveChatWebSocketUrl)(sharedChatConnection.sessionId, sharedChatConnection.lastEventId);
var socket;
try {
socket = new WebSocket(sharedChatConnection.websocketUrl);
}
catch (_a) {
handleSharedDisconnect("\uC6CC\uD06C\uC11C\uBC84 WebSocket \uC8FC\uC18C\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uB300\uC0C1: ".concat(sharedChatConnection.websocketUrl || '/ws/chat', " \uC790\uB3D9\uC73C\uB85C \uB2E4\uC2DC \uC5F0\uACB0\uD569\uB2C8\uB2E4."), 'WebSocket 객체를 생성하지 못했습니다. 대상 주소 형식과 환경변수를 확인해 주세요.');
return;
}
sharedChatConnection.socketRef.current = socket;
sharedChatConnection.suppressDisconnectNotification = false;
var disconnectHandled = false;
var reportDisconnect = function (message, closeEvent) {
if (disconnectHandled) {
return;
}
disconnectHandled = true;
var wasSuppressed = sharedChatConnection.suppressDisconnectNotification;
if (sharedChatConnection.socketRef.current === socket) {
sharedChatConnection.socketRef.current = null;
}
sharedChatConnection.suppressDisconnectNotification = false;
if (wasSuppressed) {
setSharedConnectionError('');
return;
}
if ((closeEvent === null || closeEvent === void 0 ? void 0 : closeEvent.code) === 1008) {
clearReconnectTimer();
setSharedConnectionError('등록된 접근 토큰이 없어 채팅 연결이 차단되었습니다.');
setSharedConnectionState('disconnected');
return;
}
if ((closeEvent === null || closeEvent === void 0 ? void 0 : closeEvent.code) === 1000 && !message) {
setSharedConnectionError('');
handleSharedDisconnect();
scheduleReconnect();
return;
}
void (0, chatUtils_1.diagnoseConnectionFailure)(sharedChatConnection.websocketUrl, closeEvent).then(function (detail) {
handleSharedDisconnect(message, detail);
});
};
sharedChatConnection.connectTimeoutId = window.setTimeout(function () {
if (sharedChatConnection.socketRef.current !== socket || socket.readyState === WebSocket.OPEN) {
return;
}
sharedChatConnection.socketRef.current = null;
socket.close();
reportDisconnect("\uC6CC\uD06C\uC11C\uBC84 \uC5F0\uACB0 \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB300\uC0C1: ".concat(sharedChatConnection.websocketUrl || '/ws/chat', " \uC790\uB3D9\uC73C\uB85C \uB2E4\uC2DC \uC5F0\uACB0\uD569\uB2C8\uB2E4."));
}, chatUtils_1.CHAT_CONNECTION.connectTimeoutMs);
socket.addEventListener('open', function () {
clearConnectTimeout();
clearDisconnectUiTimer();
sharedChatConnection.hasConnectedOnce = true;
sharedChatConnection.suppressDisconnectNotification = false;
setSharedConnectionState('connected');
setSharedConnectionError('');
sendContextUpdate(sharedChatConnection.currentContext);
startPresenceMonitoring();
});
socket.addEventListener('message', function (event) {
var _a, _b;
var setMessages = sharedChatConnection.setMessages;
if (!setMessages) {
return;
}
void (0, chatUtils_1.handleChatServerEvent)({
eventData: String(event.data),
currentPageUrl: (_b = (_a = sharedChatConnection.currentContext) === null || _a === void 0 ? void 0 : _a.pageUrl) !== null && _b !== void 0 ? _b : '',
expectedSessionId: sharedChatConnection.sessionId,
setMessages: setMessages,
onMessageEvent: sharedChatConnection.onMessageEvent,
onJobEvent: sharedChatConnection.onJobEvent,
onRuntimeEvent: sharedChatConnection.onRuntimeEvent,
onRuntimeDetailEvent: sharedChatConnection.onRuntimeDetailEvent,
onActivityEvent: sharedChatConnection.onActivityEvent,
onEventReceived: function (eventId) {
sharedChatConnection.lastEventId = eventId;
(0, chatUtils_1.persistLastReceivedChatEventId)(sharedChatConnection.sessionId, eventId);
sendEventReceived(eventId);
},
});
try {
var parsedEvent = JSON.parse(String(event.data));
if ((parsedEvent === null || parsedEvent === void 0 ? void 0 : parsedEvent.type) === 'chat:runtime') {
setSharedChatRuntimeSnapshot(parsedEvent.payload);
}
}
catch (_c) {
// ignore malformed payloads here; detailed parsing is already handled downstream
}
});
socket.addEventListener('close', function (event) {
clearConnectTimeout();
stopPresenceMonitoring();
reportDisconnect(event.code === 1000 ? undefined : '워크서버 연결이 끊어졌습니다. 자동으로 다시 연결합니다.', event);
});
socket.addEventListener('error', function () {
clearConnectTimeout();
stopPresenceMonitoring();
reportDisconnect('워크서버 WebSocket 연결에 실패했습니다. 자동으로 다시 연결합니다.');
});
}
function ensureSharedConnection(options) {
var sessionChanged = sharedChatConnection.sessionId !== options.sessionId;
sharedChatConnection.currentContext = options.currentContext;
sharedChatConnection.setMessages = options.setMessages;
sharedChatConnection.onMessageEvent = options.onMessageEvent;
sharedChatConnection.onJobEvent = options.onJobEvent;
sharedChatConnection.onRuntimeEvent = options.onRuntimeEvent;
sharedChatConnection.onRuntimeDetailEvent = options.onRuntimeDetailEvent;
sharedChatConnection.onActivityEvent = options.onActivityEvent;
if (sessionChanged) {
sharedChatConnection.sessionId = options.sessionId;
sharedChatConnection.lastEventId = (0, chatUtils_1.getLastReceivedChatEventId)(options.sessionId);
sharedChatConnection.hasConnectedOnce = false;
disconnectSharedSocket();
}
connectSharedSocket();
}
function useChatConnection(_a) {
var sessionId = _a.sessionId, currentContext = _a.currentContext, setMessages = _a.setMessages, onMessageEvent = _a.onMessageEvent, onJobEvent = _a.onJobEvent, onRuntimeEvent = _a.onRuntimeEvent, onRuntimeDetailEvent = _a.onRuntimeDetailEvent, onActivityEvent = _a.onActivityEvent;
var _b = (0, react_1.useState)(function () { return getSnapshot(); }), snapshot = _b[0], setSnapshot = _b[1];
(0, react_1.useEffect)(function () {
sharedChatConnection.consumerCount += 1;
return function () {
releaseSharedConnectionConsumer();
};
}, []);
(0, react_1.useEffect)(function () {
var handleSnapshotChange = function () {
setSnapshot(getSnapshot());
};
var unsubscribe = subscribeChatConnection(handleSnapshotChange);
ensureSharedConnection({
sessionId: sessionId,
currentContext: currentContext,
setMessages: setMessages,
onMessageEvent: onMessageEvent,
onJobEvent: onJobEvent,
onRuntimeEvent: onRuntimeEvent,
onRuntimeDetailEvent: onRuntimeDetailEvent,
onActivityEvent: onActivityEvent,
});
handleSnapshotChange();
return function () {
unsubscribe();
};
}, [sessionId, setMessages]);
(0, react_1.useEffect)(function () {
sharedChatConnection.currentContext = currentContext;
sharedChatConnection.setMessages = setMessages;
sharedChatConnection.onMessageEvent = onMessageEvent;
sharedChatConnection.onJobEvent = onJobEvent;
sharedChatConnection.onRuntimeEvent = onRuntimeEvent;
sharedChatConnection.onRuntimeDetailEvent = onRuntimeDetailEvent;
sharedChatConnection.onActivityEvent = onActivityEvent;
sendContextUpdate(currentContext);
}, [
currentContext,
onMessageEvent,
onJobEvent,
onRuntimeEvent,
onRuntimeDetailEvent,
onActivityEvent,
setMessages,
]);
(0, react_1.useEffect)(function () {
sharedChatConnection.pingSubscriberCount += 1;
startPresenceMonitoring();
return function () {
sharedChatConnection.pingSubscriberCount = Math.max(0, sharedChatConnection.pingSubscriberCount - 1);
if (sharedChatConnection.pingSubscriberCount === 0) {
stopPresenceMonitoring();
}
};
}, []);
return {
connectionState: snapshot.connectionState,
connectionErrorDetail: snapshot.connectionErrorDetail,
socketRef: sharedChatConnection.socketRef,
};
}

View File

@@ -483,6 +483,8 @@ function connectSharedSocket() {
if (closeEvent?.code === 1000 && !message) {
setSharedConnectionError('');
handleSharedDisconnect();
scheduleReconnect();
return;
}

View File

@@ -0,0 +1,116 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.useErrorLogs = useErrorLogs;
var react_1 = require("react");
var errorLogApi_1 = require("../errorLogApi");
var errorLogUtils_1 = require("./errorLogUtils");
function useErrorLogs(_a) {
var _this = this;
var activeView = _a.activeView, hasAccess = _a.hasAccess;
var _b = (0, react_1.useState)([]), errorLogs = _b[0], setErrorLogs = _b[1];
var _c = (0, react_1.useState)(null), selectedErrorLogId = _c[0], setSelectedErrorLogId = _c[1];
var _d = (0, react_1.useState)(false), isLoadingErrorLogs = _d[0], setIsLoadingErrorLogs = _d[1];
var _e = (0, react_1.useState)(''), errorLogLoadError = _e[0], setErrorLogLoadError = _e[1];
var _f = (0, react_1.useState)(''), activeErrorResourceUrl = _f[0], setActiveErrorResourceUrl = _f[1];
var _g = (0, react_1.useState)(false), isErrorDetailExpanded = _g[0], setIsErrorDetailExpanded = _g[1];
var loadErrorLogs = function () { return __awaiter(_this, void 0, void 0, function () {
var items_1, error_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!hasAccess || isLoadingErrorLogs) {
return [2 /*return*/];
}
setIsLoadingErrorLogs(true);
setErrorLogLoadError('');
_a.label = 1;
case 1:
_a.trys.push([1, 3, 4, 5]);
return [4 /*yield*/, (0, errorLogApi_1.fetchErrorLogs)(50)];
case 2:
items_1 = _a.sent();
setErrorLogs(items_1);
setSelectedErrorLogId(function (current) { var _a, _b; return (_b = current !== null && current !== void 0 ? current : (_a = items_1[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null; });
return [3 /*break*/, 5];
case 3:
error_1 = _a.sent();
setErrorLogLoadError(error_1 instanceof Error ? error_1.message : '에러 로그를 불러오지 못했습니다.');
return [3 /*break*/, 5];
case 4:
setIsLoadingErrorLogs(false);
return [7 /*endfinally*/];
case 5: return [2 /*return*/];
}
});
}); };
(0, react_1.useEffect)(function () {
if (activeView !== 'errors' || !hasAccess) {
return;
}
void loadErrorLogs();
}, [activeView, hasAccess]);
var selectedErrorLog = (0, react_1.useMemo)(function () { var _a, _b; return (_b = (_a = errorLogs.find(function (item) { return item.id === selectedErrorLogId; })) !== null && _a !== void 0 ? _a : errorLogs[0]) !== null && _b !== void 0 ? _b : null; }, [errorLogs, selectedErrorLogId]);
var selectedErrorLogReferenceSummary = (0, react_1.useMemo)(function () { return (selectedErrorLog ? (0, errorLogUtils_1.buildErrorReferenceSummary)(selectedErrorLog) : null); }, [selectedErrorLog]);
var activeErrorResource = (0, react_1.useMemo)(function () {
var _a, _b;
return (_a = selectedErrorLogReferenceSummary === null || selectedErrorLogReferenceSummary === void 0 ? void 0 : selectedErrorLogReferenceSummary.resources.find(function (resource) { return resource.url === activeErrorResourceUrl; })) !== null && _a !== void 0 ? _a : (0, errorLogUtils_1.getDefaultErrorResource)((_b = selectedErrorLogReferenceSummary === null || selectedErrorLogReferenceSummary === void 0 ? void 0 : selectedErrorLogReferenceSummary.resources) !== null && _b !== void 0 ? _b : []);
}, [activeErrorResourceUrl, selectedErrorLogReferenceSummary]);
var errorSourceSummary = (0, react_1.useMemo)(function () { return (0, errorLogUtils_1.buildErrorSourceSummary)(errorLogs); }, [errorLogs]);
(0, react_1.useEffect)(function () {
var _a, _b, _c;
var nextUrl = (_c = (_b = (0, errorLogUtils_1.getDefaultErrorResource)((_a = selectedErrorLogReferenceSummary === null || selectedErrorLogReferenceSummary === void 0 ? void 0 : selectedErrorLogReferenceSummary.resources) !== null && _a !== void 0 ? _a : [])) === null || _b === void 0 ? void 0 : _b.url) !== null && _c !== void 0 ? _c : '';
setActiveErrorResourceUrl(nextUrl);
}, [selectedErrorLog === null || selectedErrorLog === void 0 ? void 0 : selectedErrorLog.id, selectedErrorLogReferenceSummary]);
return {
errorLogs: errorLogs,
selectedErrorLog: selectedErrorLog,
selectedErrorLogId: selectedErrorLogId,
selectedErrorLogReferenceSummary: selectedErrorLogReferenceSummary,
activeErrorResource: activeErrorResource,
errorSourceSummary: errorSourceSummary,
isLoadingErrorLogs: isLoadingErrorLogs,
errorLogLoadError: errorLogLoadError,
activeErrorResourceUrl: activeErrorResourceUrl,
isErrorDetailExpanded: isErrorDetailExpanded,
setSelectedErrorLogId: setSelectedErrorLogId,
setActiveErrorResourceUrl: setActiveErrorResourceUrl,
setIsErrorDetailExpanded: setIsErrorDetailExpanded,
loadErrorLogs: loadErrorLogs,
};
}