feat: update codex live runtime and restart flow

This commit is contained in:
2026-04-23 18:10:43 +09:00
parent b0b9980a6c
commit 6e863feafd
36 changed files with 1636 additions and 358 deletions

View File

@@ -553,6 +553,7 @@ function InlineMessagePreview({
isPreviewLoading={isLoading}
previewError={previewError}
previewContentType={previewContentType}
maxMarkdownBlocks={12}
/>
</div>
) : null}

View File

@@ -209,6 +209,7 @@ type ChatPreviewBodyProps = {
isPreviewLoading: boolean;
previewError: string;
previewContentType?: string;
maxMarkdownBlocks?: number;
};
function isHtmlFallbackPreview(target: ChatPreviewTarget, previewText: string, previewContentType: string | undefined) {
@@ -236,6 +237,7 @@ export function ChatPreviewBody({
isPreviewLoading,
previewError,
previewContentType,
maxMarkdownBlocks,
}: ChatPreviewBodyProps) {
if (!target) {
return <Empty description="preview 가능한 링크가 아직 없습니다." />;
@@ -294,7 +296,10 @@ export function ChatPreviewBody({
if (target.kind === 'markdown') {
return (
<div className="app-chat-panel__preview-rich app-chat-panel__preview-rich--markdown">
<MarkdownPreviewContent content={previewText || '# Preview\n\n표시할 preview 본문이 없습니다.'} maxBlocks={12} />
<MarkdownPreviewContent
content={previewText || '# Preview\n\n표시할 preview 본문이 없습니다.'}
maxBlocks={maxMarkdownBlocks}
/>
</div>
);
}

View File

@@ -1,10 +1,11 @@
import { ClockCircleOutlined, EyeOutlined, LoadingOutlined, StopOutlined } from '@ant-design/icons';
import { Button, Drawer, Empty, Modal, Space, Typography } from 'antd';
import { ClockCircleOutlined, EyeOutlined, LoadingOutlined, StopOutlined, UndoOutlined } from '@ant-design/icons';
import { Button, Drawer, Empty, Modal, Space, Typography, message } from 'antd';
import { useEffect, useRef, useState } from 'react';
import {
cancelChatRuntimeJob,
fetchChatRuntimeJobDetail,
removeChatRuntimeJob,
rollbackChatRuntimeJob,
} from './chatUtils';
import type { ChatRuntimeJobDetail, ChatRuntimeJobItem, ChatRuntimeSnapshot } from './types';
@@ -157,10 +158,14 @@ function RecentRuntimeList({
items,
onSelectSession,
onOpenLog,
onRollbackJob,
pendingActionRequestId,
}: {
items: ChatRuntimeSnapshot['recent'];
onSelectSession: (sessionId: string) => void;
onOpenLog: (requestId: string) => void;
onRollbackJob: (requestId: string, sessionId: string) => void;
pendingActionRequestId: string | null;
}) {
return (
<section className="app-chat-runtime__section app-chat-runtime__section--recent">
@@ -181,22 +186,35 @@ function RecentRuntimeList({
<Text strong>{buildTerminalLabel(item.terminalStatus)}</Text>
<Text type="secondary">{item.mode === 'direct' ? '즉시' : '큐'}</Text>
</div>
<Button
size="small"
onClick={() => {
onOpenLog(item.requestId);
}}
>
</Button>
<Button
size="small"
onClick={() => {
onSelectSession(item.sessionId);
}}
>
</Button>
<Space size={8} wrap className="app-chat-runtime__job-actions">
<Button
size="small"
onClick={() => {
onOpenLog(item.requestId);
}}
>
</Button>
<Button
size="small"
icon={<UndoOutlined />}
disabled={item.terminalStatus !== 'completed'}
loading={pendingActionRequestId === item.requestId}
onClick={() => {
onRollbackJob(item.requestId, item.sessionId);
}}
>
</Button>
<Button
size="small"
onClick={() => {
onSelectSession(item.sessionId);
}}
>
</Button>
</Space>
</div>
<Text className="app-chat-runtime__job-summary">{item.summary || '요약 없음'}</Text>
<div className="app-chat-runtime__job-meta">
@@ -233,6 +251,8 @@ export function ChatRuntimeDashboard({
onRequestedLogHandled?: () => void;
}) {
const sessions = snapshot?.sessions ?? [];
const [messageApi, messageContextHolder] = message.useMessage();
const [modalApi, modalContextHolder] = Modal.useModal();
const [selectedDetail, setSelectedDetail] = useState<ChatRuntimeJobDetail | null>(null);
const [isLogModalOpen, setIsLogModalOpen] = useState(false);
const [logLoadError, setLogLoadError] = useState('');
@@ -240,6 +260,23 @@ export function ChatRuntimeDashboard({
const [pendingActionRequestId, setPendingActionRequestId] = useState<string | null>(null);
const logViewerRef = useRef<HTMLPreElement | null>(null);
const confirmAction = (options: {
title: string;
content: string;
okText: string;
cancelText?: string;
}) =>
new Promise<boolean>((resolve) => {
modalApi.confirm({
title: options.title,
content: options.content,
okText: options.okText,
cancelText: options.cancelText ?? '닫기',
onOk: () => resolve(true),
onCancel: () => resolve(false),
});
});
const loadLogDetail = async (requestId: string) => {
setIsLogLoading(true);
setLogLoadError('');
@@ -261,15 +298,10 @@ export function ChatRuntimeDashboard({
};
const handleCancel = async (requestId: string) => {
const confirmed = await new Promise<boolean>((resolve) => {
Modal.confirm({
title: '실행 중 요청을 취소할까요?',
content: '이미 실행 중인 Codex 프로세스에 종료 신호를 보냅니다.',
okText: '취소 실행',
cancelText: '닫기',
onOk: () => resolve(true),
onCancel: () => resolve(false),
});
const confirmed = await confirmAction({
title: '실행 중 요청을 취소할까요?',
content: '이미 실행 중인 Codex 프로세스에 종료 신호를 보냅니다.',
okText: '취소 실행',
});
if (!confirmed) {
@@ -280,21 +312,22 @@ export function ChatRuntimeDashboard({
try {
await cancelChatRuntimeJob(requestId);
messageApi.success('취소 요청을 보냈습니다.');
if (selectedDetail?.item?.requestId === requestId) {
await loadLogDetail(requestId);
}
} catch (error) {
messageApi.error(error instanceof Error ? error.message : '실행 취소 요청에 실패했습니다.');
} finally {
setPendingActionRequestId(null);
}
};
const handleRemove = async (requestId: string) => {
const confirmed = await new Promise<boolean>((resolve) => {
Modal.confirm({
title: '대기 요청 제거할까요?',
content: '아직 실행되지 않은 대기 요청만 제거됩니다.',
okText: '제거',
cancelText: '닫기',
onOk: () => resolve(true),
onCancel: () => resolve(false),
});
const confirmed = await confirmAction({
title: '대기열 요청을 제거할까요?',
content: '아직 실행되지 않은 대기 요청 제거됩니다.',
okText: '제거',
});
if (!confirmed) {
@@ -305,6 +338,38 @@ export function ChatRuntimeDashboard({
try {
await removeChatRuntimeJob(requestId);
messageApi.success('대기 요청을 제거했습니다.');
if (selectedDetail?.item?.requestId === requestId) {
await loadLogDetail(requestId);
}
} catch (error) {
messageApi.error(error instanceof Error ? error.message : '대기 요청 제거에 실패했습니다.');
} finally {
setPendingActionRequestId(null);
}
};
const handleRollback = async (requestId: string, sessionId: string) => {
const confirmed = await confirmAction({
title: '최근 실행 변경을 롤백할까요?',
content: '이 실행이 남긴 diff만 역적용합니다. 이후 다른 세션이 같은 라인을 수정했다면 실패할 수 있습니다.',
okText: '롤백',
});
if (!confirmed) {
return;
}
setPendingActionRequestId(requestId);
try {
await rollbackChatRuntimeJob(requestId, sessionId);
messageApi.success('최근 실행 변경을 롤백했습니다.');
if (selectedDetail?.item?.requestId === requestId) {
await loadLogDetail(requestId);
}
} catch (error) {
messageApi.error(error instanceof Error ? error.message : '최근 실행 롤백에 실패했습니다.');
} finally {
setPendingActionRequestId(null);
}
@@ -362,6 +427,8 @@ export function ChatRuntimeDashboard({
return (
<>
{messageContextHolder}
{modalContextHolder}
<div className="app-chat-runtime">
<div className="app-chat-runtime__summary-strip">
<div className="app-chat-runtime__summary-card">
@@ -431,7 +498,13 @@ export function ChatRuntimeDashboard({
onRemoveJob={handleRemove}
pendingActionRequestId={pendingActionRequestId}
/>
<RecentRuntimeList items={snapshot?.recent ?? []} onSelectSession={onSelectSession} onOpenLog={openLog} />
<RecentRuntimeList
items={snapshot?.recent ?? []}
onSelectSession={onSelectSession}
onOpenLog={openLog}
onRollbackJob={handleRollback}
pendingActionRequestId={pendingActionRequestId}
/>
</div>
</div>

View File

@@ -1003,6 +1003,19 @@ export async function removeChatRuntimeJob(requestId: string) {
return response.removed;
}
export async function rollbackChatRuntimeJob(requestId: string, sessionId?: string | null) {
const response = await requestChatApi<{ ok: boolean; rolledBack: boolean }>(
`/runtime/jobs/${encodeURIComponent(requestId)}/rollback`,
{
method: 'POST',
body: JSON.stringify({
sessionId: sessionId?.trim() || undefined,
}),
},
);
return response.rolledBack;
}
export async function uploadChatComposerFile(sessionId: string, file: File) {
const normalizedSessionId = sessionId.trim();
@@ -1028,6 +1041,7 @@ export async function createChatConversationRoom(args: {
sessionId: string;
title?: string;
chatTypeId?: string | null;
lastChatTypeId?: string | null;
contextLabel?: string;
contextDescription?: string;
notifyOffline?: boolean;
@@ -1040,6 +1054,7 @@ export async function createChatConversationRoom(args: {
sessionId: args.sessionId,
title: args.title ?? '새 대화',
chatTypeId: args.chatTypeId ?? null,
lastChatTypeId: args.lastChatTypeId ?? args.chatTypeId ?? null,
contextLabel: args.contextLabel ?? null,
contextDescription: args.contextDescription ?? null,
notifyOffline,
@@ -1076,6 +1091,7 @@ export async function updateChatConversationRoom(
payload: {
title?: string;
chatTypeId?: string | null;
lastChatTypeId?: string | null;
contextLabel?: string | null;
contextDescription?: string | null;
notifyOffline?: boolean;

View File

@@ -38,6 +38,7 @@ export type ChatConversationSummary = {
clientId: string | null;
title: string;
chatTypeId: string | null;
lastChatTypeId: string | null;
contextLabel: string | null;
contextDescription: string | null;
notifyOffline: boolean;