feat: refine codex live chat context flows
This commit is contained in:
@@ -50,6 +50,8 @@ export type ChatConversationItem = {
|
||||
currentJobMessage: string | null;
|
||||
currentQueueSize: number;
|
||||
currentStatusUpdatedAt: string | null;
|
||||
isPendingWork: boolean;
|
||||
pendingWorkReason: 'prompt' | 'analysis' | 'design' | null;
|
||||
lastRequestPreview: string;
|
||||
lastMessagePreview: string;
|
||||
lastResponsePreview: string;
|
||||
@@ -173,6 +175,160 @@ function createPreview(text: string) {
|
||||
return normalized.length > 140 ? `${normalized.slice(0, 137).trimEnd()}...` : normalized;
|
||||
}
|
||||
|
||||
const PENDING_WORK_ANALYSIS_PATTERNS = [
|
||||
/분석/u,
|
||||
/검토/u,
|
||||
/조사/u,
|
||||
/원인/u,
|
||||
/파악/u,
|
||||
/\banalysis\b/i,
|
||||
/\binvestigat(?:e|ion)\b/i,
|
||||
] as const;
|
||||
|
||||
const PENDING_WORK_DESIGN_PATTERNS = [
|
||||
/설계/u,
|
||||
/프롬프트/u,
|
||||
/시안/u,
|
||||
/구조/u,
|
||||
/방향/u,
|
||||
/기획/u,
|
||||
/플로우/u,
|
||||
/아키텍처/u,
|
||||
/\bdesign\b/i,
|
||||
/\barchitecture\b/i,
|
||||
] as const;
|
||||
|
||||
const PENDING_WORK_IMPLEMENTATION_PATTERNS = [
|
||||
/구현했/u,
|
||||
/수정했/u,
|
||||
/반영했/u,
|
||||
/적용했/u,
|
||||
/완료했/u,
|
||||
/마무리했/u,
|
||||
/배포했/u,
|
||||
/검증했/u,
|
||||
/빌드.*통과/u,
|
||||
/테스트.*통과/u,
|
||||
/캡처/u,
|
||||
/preview/iu,
|
||||
/변경 파일/u,
|
||||
/diff/u,
|
||||
/\bimplement(?:ed|ation)?\b/i,
|
||||
/\bfix(?:ed)?\b/i,
|
||||
/\bverified?\b/i,
|
||||
/\btested?\b/i,
|
||||
] as const;
|
||||
|
||||
const PENDING_WORK_RESPONSE_HOLD_PATTERNS = [
|
||||
/원하시면/u,
|
||||
/진행해드릴/u,
|
||||
/이어(?:서|가)/u,
|
||||
/다음 단계/u,
|
||||
/선택/u,
|
||||
/옵션/u,
|
||||
/후속/u,
|
||||
/\bif you want\b/i,
|
||||
/\bnext step\b/i,
|
||||
] as const;
|
||||
|
||||
function normalizePendingWorkText(text: string | null | undefined) {
|
||||
return String(text ?? '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function hasPendingWorkPattern(text: string, patterns: readonly RegExp[]) {
|
||||
return patterns.some((pattern) => pattern.test(text));
|
||||
}
|
||||
|
||||
function resolvePendingWorkReasonFromText(text: string) {
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasPendingWorkPattern(text, PENDING_WORK_DESIGN_PATTERNS)) {
|
||||
return 'design' as const;
|
||||
}
|
||||
|
||||
if (hasPendingWorkPattern(text, PENDING_WORK_ANALYSIS_PATTERNS)) {
|
||||
return 'analysis' as const;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasOpenPromptParts(parts: ChatMessagePart[] | undefined) {
|
||||
return (parts ?? []).some((part) => {
|
||||
if (part.type !== 'prompt' || part.readOnly === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((part.selectedValues?.length ?? 0) > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((part.resultText?.trim() ?? '').length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((part.resolvedAt?.trim() ?? '').length > 0 || part.resolvedBy != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function resolvePendingWorkState(args: {
|
||||
requestText?: string | null;
|
||||
responseText?: string | null;
|
||||
latestCodexParts?: ChatMessagePart[] | undefined;
|
||||
}) {
|
||||
if (hasOpenPromptParts(args.latestCodexParts)) {
|
||||
return {
|
||||
isPendingWork: true,
|
||||
pendingWorkReason: 'prompt' as const,
|
||||
};
|
||||
}
|
||||
|
||||
const requestText = normalizePendingWorkText(args.requestText);
|
||||
const responseText = normalizePendingWorkText(args.responseText);
|
||||
const requestReason = resolvePendingWorkReasonFromText(requestText);
|
||||
|
||||
if (!requestReason) {
|
||||
return {
|
||||
isPendingWork: false,
|
||||
pendingWorkReason: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (hasPendingWorkPattern(responseText, PENDING_WORK_IMPLEMENTATION_PATTERNS)) {
|
||||
return {
|
||||
isPendingWork: false,
|
||||
pendingWorkReason: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (!responseText) {
|
||||
return {
|
||||
isPendingWork: true,
|
||||
pendingWorkReason: requestReason,
|
||||
};
|
||||
}
|
||||
|
||||
const responseReason = resolvePendingWorkReasonFromText(responseText);
|
||||
|
||||
if (responseReason || hasPendingWorkPattern(responseText, PENDING_WORK_RESPONSE_HOLD_PATTERNS)) {
|
||||
return {
|
||||
isPendingWork: true,
|
||||
pendingWorkReason: responseReason ?? requestReason,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isPendingWork: false,
|
||||
pendingWorkReason: null,
|
||||
};
|
||||
}
|
||||
|
||||
const CONTEXT_DEPENDENT_REQUEST_PATTERNS = [
|
||||
/이전\s*(채팅|대화|문맥)/u,
|
||||
/이전\s*요청/u,
|
||||
@@ -279,6 +435,8 @@ function mapConversationRow(row: Record<string, unknown>): ChatConversationItem
|
||||
currentJobMessage: row.current_job_message == null ? null : String(row.current_job_message),
|
||||
currentQueueSize: Number(row.current_queue_size ?? 0),
|
||||
currentStatusUpdatedAt: normalizeDateTimeValue(row.current_status_updated_at),
|
||||
isPendingWork: false,
|
||||
pendingWorkReason: null,
|
||||
lastRequestPreview: '',
|
||||
lastMessagePreview: String(row.last_message_preview ?? ''),
|
||||
lastResponsePreview: '',
|
||||
@@ -876,6 +1034,40 @@ async function getLatestResponseMessageIdMap(sessionIds: string[]) {
|
||||
return responseMap;
|
||||
}
|
||||
|
||||
async function getLatestCodexPromptPartsMap(sessionIds: string[]) {
|
||||
const normalizedSessionIds = Array.from(new Set(sessionIds.map((sessionId) => sessionId.trim()).filter(Boolean)));
|
||||
|
||||
if (normalizedSessionIds.length === 0) {
|
||||
return new Map<string, ChatMessagePart[]>();
|
||||
}
|
||||
|
||||
const rows = await db(CHAT_CONVERSATION_MESSAGE_TABLE)
|
||||
.select('session_id', 'parts_json', 'created_at', 'message_id')
|
||||
.whereIn('session_id', normalizedSessionIds)
|
||||
.andWhere('author', 'codex')
|
||||
.orderBy('session_id', 'asc')
|
||||
.orderBy('created_at', 'desc')
|
||||
.orderBy('message_id', 'desc');
|
||||
|
||||
const promptPartMap = new Map<string, ChatMessagePart[]>();
|
||||
|
||||
for (const row of rows) {
|
||||
const sessionId = String(row.session_id ?? '').trim();
|
||||
|
||||
if (!sessionId || promptPartMap.has(sessionId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parts = parseChatMessageParts(row.parts_json);
|
||||
|
||||
if ((parts ?? []).some((part) => part.type === 'prompt')) {
|
||||
promptPartMap.set(sessionId, parts ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
return promptPartMap;
|
||||
}
|
||||
|
||||
async function getLatestResponseMessageId(sessionId: string) {
|
||||
const responseMap = await getLatestResponseMessageIdMap([sessionId]);
|
||||
return responseMap.get(sessionId.trim()) ?? null;
|
||||
@@ -1444,17 +1636,26 @@ export async function listChatConversations(
|
||||
const latestResponseMessageIdMap = await getLatestResponseMessageIdMap(
|
||||
rows.map((row) => String(row.session_id ?? '')),
|
||||
);
|
||||
const latestCodexPromptPartsMap = await getLatestCodexPromptPartsMap(
|
||||
rows.map((row) => String(row.session_id ?? '')),
|
||||
);
|
||||
|
||||
if (!normalizedUnreadStateClientId) {
|
||||
return rows
|
||||
.map((row) => {
|
||||
const mapped = mapConversationRow(row);
|
||||
const pendingWorkState = resolvePendingWorkState({
|
||||
requestText: latestRequestPreviewMap.get(mapped.sessionId)?.text ?? '',
|
||||
responseText: latestResponsePreviewMap.get(mapped.sessionId)?.text ?? '',
|
||||
latestCodexParts: latestCodexPromptPartsMap.get(mapped.sessionId),
|
||||
});
|
||||
return {
|
||||
...resolveConversationPreviewOverride(
|
||||
mapped,
|
||||
latestPreviewMessageMap.get(mapped.sessionId),
|
||||
latestRequestPreviewMap.get(mapped.sessionId),
|
||||
),
|
||||
...pendingWorkState,
|
||||
lastRequestPreview: createPreview(latestRequestPreviewMap.get(mapped.sessionId)?.text ?? ''),
|
||||
lastResponsePreview: createPreview(latestResponsePreviewMap.get(mapped.sessionId)?.text ?? ''),
|
||||
hasUnreadResponse: false,
|
||||
@@ -1489,6 +1690,11 @@ export async function listChatConversations(
|
||||
const mapped = mapConversationRow(row);
|
||||
const preference = preferenceMap.get(mapped.sessionId);
|
||||
const latestPreviewMessage = latestPreviewMessageMap.get(mapped.sessionId);
|
||||
const pendingWorkState = resolvePendingWorkState({
|
||||
requestText: latestRequestPreviewMap.get(mapped.sessionId)?.text ?? '',
|
||||
responseText: latestResponsePreviewMap.get(mapped.sessionId)?.text ?? '',
|
||||
latestCodexParts: latestCodexPromptPartsMap.get(mapped.sessionId),
|
||||
});
|
||||
|
||||
return {
|
||||
...resolveConversationPreviewOverride(
|
||||
@@ -1496,6 +1702,7 @@ export async function listChatConversations(
|
||||
latestPreviewMessage,
|
||||
latestRequestPreviewMap.get(mapped.sessionId),
|
||||
),
|
||||
...pendingWorkState,
|
||||
lastRequestPreview: createPreview(latestRequestPreviewMap.get(mapped.sessionId)?.text ?? ''),
|
||||
lastResponsePreview: createPreview(latestResponsePreviewMap.get(mapped.sessionId)?.text ?? ''),
|
||||
clientId: normalizedUnreadStateClientId,
|
||||
@@ -1654,7 +1861,7 @@ export async function listChatConversationDetailPage(
|
||||
): Promise<ChatConversationDetailPage> {
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const conversation = await db(CHAT_CONVERSATION_TABLE).where({ session_id: normalizedSessionId }).first();
|
||||
const normalizedLimit = Math.max(1, Math.min(100, Math.round(options.limit ?? 6)));
|
||||
const normalizedLimit = Math.max(1, Math.min(100, Math.round(options.limit ?? 8)));
|
||||
const normalizedBeforeMessageId =
|
||||
Number.isFinite(options.beforeMessageId) && (options.beforeMessageId ?? 0) > 0
|
||||
? Math.trunc(options.beforeMessageId as number)
|
||||
@@ -2435,6 +2642,36 @@ export async function deleteChatConversation(sessionId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearChatConversationData(sessionId: string, clientId?: string | null) {
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx(CHAT_CONVERSATION_ACTIVITY_TABLE).where({ session_id: normalizedSessionId }).del();
|
||||
await trx(CHAT_CONVERSATION_REQUEST_TABLE).where({ session_id: normalizedSessionId }).del();
|
||||
await trx(CHAT_CONVERSATION_MESSAGE_TABLE).where({ session_id: normalizedSessionId }).del();
|
||||
await trx(CHAT_CONVERSATION_CLIENT_TABLE)
|
||||
.where({ session_id: normalizedSessionId })
|
||||
.update({
|
||||
last_read_response_message_id: null,
|
||||
updated_at: db.fn.now(),
|
||||
});
|
||||
await trx(CHAT_CONVERSATION_TABLE)
|
||||
.where({ session_id: normalizedSessionId })
|
||||
.update({
|
||||
current_request_id: null,
|
||||
current_job_status: null,
|
||||
current_job_message: null,
|
||||
current_queue_size: 0,
|
||||
current_status_updated_at: null,
|
||||
last_message_preview: '',
|
||||
last_message_at: null,
|
||||
updated_at: db.fn.now(),
|
||||
});
|
||||
});
|
||||
|
||||
return getChatConversation(normalizedSessionId, clientId);
|
||||
}
|
||||
|
||||
export async function getChatConversationClientPreference(sessionId: string, clientId: string) {
|
||||
const row = await db(CHAT_CONVERSATION_CLIENT_TABLE)
|
||||
.where({
|
||||
|
||||
Reference in New Issue
Block a user