feat: update codex live runtime and restart flow
This commit is contained in:
@@ -553,6 +553,7 @@ function InlineMessagePreview({
|
||||
isPreviewLoading={isLoading}
|
||||
previewError={previewError}
|
||||
previewContentType={previewContentType}
|
||||
maxMarkdownBlocks={12}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user