From 1d0eae769a28050322b05426c7f5dabc584feb9e Mon Sep 17 00:00:00 2001 From: Variet Date: Sun, 8 Mar 2026 00:34:24 +0900 Subject: [PATCH] =?UTF-8?q?refactor(ui):=20Antigravity-style=20conversatio?= =?UTF-8?q?n=20=E2=80=94=20task=20cards=20group=20tool=20actions,=20remove?= =?UTF-8?q?=20internal=20steps,=20mode=20badges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/css/style.css | 24 +++++++++ public/js/app.js | 105 +++++++++++++++++++++++++--------------- public/js/chat-panel.js | 36 ++++++++++---- 3 files changed, 116 insertions(+), 49 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index d3eb44f..734a703 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -500,6 +500,30 @@ body { flex: 1; min-width: 0; word-break: break-word; + font-family: 'JetBrains Mono', 'Cascadia Code', monospace; + font-size: 11px; + color: var(--text-secondary); +} + +.msg-step-check { + flex-shrink: 0; + font-size: 11px; + margin-left: 6px; +} + +.msg-mode-badge { + margin-left: 6px; + font-size: 12px; +} + +.md-code { + background: var(--bg-tertiary); + border-radius: 6px; + padding: 8px 12px; + font-family: 'JetBrains Mono', 'Cascadia Code', monospace; + font-size: 12px; + overflow-x: auto; + margin: 4px 0; } /* --- 텍스트 메시지 --- */ diff --git a/public/js/app.js b/public/js/app.js index 0902107..7be96ba 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -159,20 +159,46 @@ } /** - * Trajectory 스텝 배열을 ChatPanel message[] 형식으로 변환 + * Trajectory 스텝을 Antigravity 스타일 대화 흐름으로 변환 + * + * 규칙: + * - USER_INPUT → 사용자 턴 구분자 (새 대화 블록 시작) + * - NOTIFY_USER → AI의 최종 응답 (마크다운) + * - TASK_BOUNDARY → 작업 카드 (이후 도구 스텝들을 내부 요약으로 포함) + * - RUN_COMMAND, CODE_ACTION → task 카드 내부 도구 호출 요약 + * - 기타 (PLANNER_RESPONSE, EPHEMERAL, VIEW_FILE 등) → 건너뛰기 */ function parseTrajectoryToMessages(steps) { const msgs = []; + let currentTask = null; // 현재 활성 task 카드 + let toolActions = []; // 현재 task 내 도구 호출들 + + function flushTask() { + if (currentTask) { + currentTask.tools = [...toolActions]; + msgs.push(currentTask); + currentTask = null; + toolActions = []; + } + } + for (const step of steps) { switch (step.type) { case 'CORTEX_STEP_TYPE_USER_INPUT': { + // 이전 task 플러시 + flushTask(); + // 사용자 턴 구분 const text = step.userInput?.items?.[0]?.chunk?.value || ''; - if (text) { - msgs.push({ type: 'user', text }); - } + msgs.push({ + type: 'user', + text: text || '(사용자 입력)', + time: step.metadata?.createdAt || '', + }); break; } case 'CORTEX_STEP_TYPE_NOTIFY_USER': { + // 이전 task 플러시 + flushTask(); const content = step.notifyUser?.notificationContent || ''; if (content) { msgs.push({ @@ -184,13 +210,32 @@ } case 'CORTEX_STEP_TYPE_TASK_BOUNDARY': { const tb = step.taskBoundary || {}; - if (tb.taskName) { - msgs.push({ + if (!tb.taskName) break; + // 같은 이름의 task는 업데이트 (중복 카드 방지) + if (currentTask && currentTask.title === tb.taskName) { + currentTask.summary = tb.taskSummary || currentTask.summary; + currentTask.status = tb.taskStatus || currentTask.status; + currentTask.mode = tb.mode || currentTask.mode; + } else { + flushTask(); + currentTask = { type: 'task', - title: tb.taskName || '', + title: tb.taskName, summary: tb.taskSummary || '', status: tb.taskStatus || '', mode: tb.mode || '', + tools: [], + }; + } + break; + } + case 'CORTEX_STEP_TYPE_RUN_COMMAND': { + const cmd = step.runCommand?.commandLine || step.runCommand?.command || ''; + if (cmd) { + toolActions.push({ + icon: '⚡', + label: cmd.length > 60 ? cmd.substring(0, 57) + '...' : cmd, + done: step.status === 'CORTEX_STEP_STATUS_DONE', }); } break; @@ -198,55 +243,37 @@ case 'CORTEX_STEP_TYPE_CODE_ACTION': { const ca = step.codeAction || {}; const file = ca.filePath || ca.targetFile || ''; - const desc = ca.description || ''; - if (file || desc) { - msgs.push({ - type: 'code', - language: '', - code: '', - description: `📝 ${file ? file.split(/[\\/]/).pop() : '파일'}: ${desc || '코드 수정'}`, - }); - } + const desc = ca.description || '코드 수정'; + toolActions.push({ + icon: '📝', + label: file ? file.split(/[\\/]/).pop() + ': ' + desc : desc, + done: step.status === 'CORTEX_STEP_STATUS_DONE', + }); break; } - case 'CORTEX_STEP_TYPE_RUN_COMMAND': { - const cmd = step.runCommand?.commandLine || step.runCommand?.command || ''; - if (cmd) { - msgs.push({ - type: 'code', - language: 'bash', - code: cmd, - description: `⚡ 명령 실행 (${step.status === 'CORTEX_STEP_STATUS_DONE' ? '완료' : '실행 중'})`, - }); - } + case 'CORTEX_STEP_TYPE_VIEW_FILE': + case 'CORTEX_STEP_TYPE_LIST_DIRECTORY': { + // 카운트만 — 개별 표시 안 함 break; } - case 'CORTEX_STEP_TYPE_PLANNER_RESPONSE': { - // 에이전트 사고 과정 — 접기 가능 - const text = step.plannerResponse?.text || step.plannerResponse?.rawText || ''; - if (text && text.length > 10) { - msgs.push({ - type: 'thought', - text: text.substring(0, 300) + (text.length > 300 ? '...' : ''), - }); - } - break; - } - // 덜 중요한 스텝은 생략 (VIEW_FILE, LIST_DIRECTORY, EPHEMERAL 등) + // PLANNER_RESPONSE, EPHEMERAL, CHECKPOINT, CONVERSATION_HISTORY, KNOWLEDGE_ARTIFACTS → 건너뛰기 } } + flushTask(); return msgs; } function simpleMarkdown(text) { return text .replace(/&/g, '&').replace(//g, '>') + .replace(/```(\w*)\n([\s\S]*?)```/g, '
$2
') .replace(/\n/g, '
') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/`([^`]+)`/g, '$1') .replace(/^## (.*)/gm, '

$1

') .replace(/^### (.*)/gm, '

$1

') - .replace(/\| (.+?) \|/g, (m) => `${m}`); + .replace(/^\- (.*)/gm, '
  • $1
  • ') + .replace(/^\d+\. (.*)/gm, '
  • $1
  • '); } // ─── 서버 메시지 핸들러 ─────────────────────────────── diff --git a/public/js/chat-panel.js b/public/js/chat-panel.js index 90e2d54..162c1ee 100644 --- a/public/js/chat-panel.js +++ b/public/js/chat-panel.js @@ -122,7 +122,7 @@ class ChatPanel { const toggle = document.createElement('span'); toggle.className = 'msg-toggle'; - toggle.textContent = msg.collapsed ? '▸' : '▾'; + toggle.textContent = '▾'; const title = document.createElement('span'); title.className = 'msg-card-title'; @@ -131,7 +131,17 @@ class ChatPanel { header.appendChild(toggle); header.appendChild(title); - // 요약 + // 모드 뱃지 + if (msg.mode) { + const badge = document.createElement('span'); + badge.className = 'msg-mode-badge'; + const modeMap = { PLANNING: '📋', EXECUTION: '⚙️', VERIFICATION: '✅' }; + badge.textContent = modeMap[msg.mode] || msg.mode; + badge.title = msg.mode; + header.appendChild(badge); + } + + // 요약 if (msg.summary) { const summary = document.createElement('div'); summary.className = 'msg-card-summary'; @@ -141,26 +151,32 @@ class ChatPanel { card.appendChild(header); - // 하위 항목 - if (msg.steps && msg.steps.length > 0) { + // 도구 호출 목록 (tools) + const toolList = msg.tools || msg.steps || []; + if (toolList.length > 0) { const body = document.createElement('div'); body.className = 'msg-card-body'; - if (msg.collapsed) body.style.display = 'none'; - for (const step of msg.steps) { + for (const tool of toolList) { const row = document.createElement('div'); row.className = 'msg-step'; const icon = document.createElement('span'); icon.className = 'msg-step-icon'; - icon.textContent = step.icon || '•'; + icon.textContent = tool.icon || '•'; const text = document.createElement('span'); text.className = 'msg-step-text'; - text.textContent = step.text; + text.textContent = tool.label || tool.text || ''; + + const check = document.createElement('span'); + check.className = 'msg-step-check'; + check.textContent = tool.done ? '✓' : '…'; + check.style.color = tool.done ? '#22c55e' : '#94a3b8'; row.appendChild(icon); row.appendChild(text); + row.appendChild(check); body.appendChild(row); } @@ -175,7 +191,7 @@ class ChatPanel { }); } - // 카드 내부 액션 버튼 (Cancel, Review Changes 등) + // 카드 내부 액션 버튼 if (msg.actions && msg.actions.length > 0) { const actionsDiv = document.createElement('div'); actionsDiv.className = 'msg-card-actions'; @@ -192,7 +208,7 @@ class ChatPanel { if (btn.x && btn.y) { el.style.cursor = 'pointer'; el.addEventListener('click', (e) => { - e.stopPropagation(); // 카드 토글 방지 + e.stopPropagation(); if (this.onActionClick) { this.onActionClick({ label: btn.label, x: btn.x, y: btn.y }); }