refactor(ui): Antigravity-style conversation — task cards group tool actions, remove internal steps, mode badges
This commit is contained in:
@@ -500,6 +500,30 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
word-break: break-word;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- 텍스트 메시지 --- */
|
/* --- 텍스트 메시지 --- */
|
||||||
|
|||||||
103
public/js/app.js
103
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) {
|
function parseTrajectoryToMessages(steps) {
|
||||||
const msgs = [];
|
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) {
|
for (const step of steps) {
|
||||||
switch (step.type) {
|
switch (step.type) {
|
||||||
case 'CORTEX_STEP_TYPE_USER_INPUT': {
|
case 'CORTEX_STEP_TYPE_USER_INPUT': {
|
||||||
|
// 이전 task 플러시
|
||||||
|
flushTask();
|
||||||
|
// 사용자 턴 구분
|
||||||
const text = step.userInput?.items?.[0]?.chunk?.value || '';
|
const text = step.userInput?.items?.[0]?.chunk?.value || '';
|
||||||
if (text) {
|
msgs.push({
|
||||||
msgs.push({ type: 'user', text });
|
type: 'user',
|
||||||
}
|
text: text || '(사용자 입력)',
|
||||||
|
time: step.metadata?.createdAt || '',
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'CORTEX_STEP_TYPE_NOTIFY_USER': {
|
case 'CORTEX_STEP_TYPE_NOTIFY_USER': {
|
||||||
|
// 이전 task 플러시
|
||||||
|
flushTask();
|
||||||
const content = step.notifyUser?.notificationContent || '';
|
const content = step.notifyUser?.notificationContent || '';
|
||||||
if (content) {
|
if (content) {
|
||||||
msgs.push({
|
msgs.push({
|
||||||
@@ -184,13 +210,32 @@
|
|||||||
}
|
}
|
||||||
case 'CORTEX_STEP_TYPE_TASK_BOUNDARY': {
|
case 'CORTEX_STEP_TYPE_TASK_BOUNDARY': {
|
||||||
const tb = step.taskBoundary || {};
|
const tb = step.taskBoundary || {};
|
||||||
if (tb.taskName) {
|
if (!tb.taskName) break;
|
||||||
msgs.push({
|
// 같은 이름의 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',
|
type: 'task',
|
||||||
title: tb.taskName || '',
|
title: tb.taskName,
|
||||||
summary: tb.taskSummary || '',
|
summary: tb.taskSummary || '',
|
||||||
status: tb.taskStatus || '',
|
status: tb.taskStatus || '',
|
||||||
mode: tb.mode || '',
|
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;
|
break;
|
||||||
@@ -198,55 +243,37 @@
|
|||||||
case 'CORTEX_STEP_TYPE_CODE_ACTION': {
|
case 'CORTEX_STEP_TYPE_CODE_ACTION': {
|
||||||
const ca = step.codeAction || {};
|
const ca = step.codeAction || {};
|
||||||
const file = ca.filePath || ca.targetFile || '';
|
const file = ca.filePath || ca.targetFile || '';
|
||||||
const desc = ca.description || '';
|
const desc = ca.description || '코드 수정';
|
||||||
if (file || desc) {
|
toolActions.push({
|
||||||
msgs.push({
|
icon: '📝',
|
||||||
type: 'code',
|
label: file ? file.split(/[\\/]/).pop() + ': ' + desc : desc,
|
||||||
language: '',
|
done: step.status === 'CORTEX_STEP_STATUS_DONE',
|
||||||
code: '',
|
|
||||||
description: `📝 ${file ? file.split(/[\\/]/).pop() : '파일'}: ${desc || '코드 수정'}`,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'CORTEX_STEP_TYPE_RUN_COMMAND': {
|
case 'CORTEX_STEP_TYPE_VIEW_FILE':
|
||||||
const cmd = step.runCommand?.commandLine || step.runCommand?.command || '';
|
case 'CORTEX_STEP_TYPE_LIST_DIRECTORY': {
|
||||||
if (cmd) {
|
// 카운트만 — 개별 표시 안 함
|
||||||
msgs.push({
|
|
||||||
type: 'code',
|
|
||||||
language: 'bash',
|
|
||||||
code: cmd,
|
|
||||||
description: `⚡ 명령 실행 (${step.status === 'CORTEX_STEP_STATUS_DONE' ? '완료' : '실행 중'})`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'CORTEX_STEP_TYPE_PLANNER_RESPONSE': {
|
// PLANNER_RESPONSE, EPHEMERAL, CHECKPOINT, CONVERSATION_HISTORY, KNOWLEDGE_ARTIFACTS → 건너뛰기
|
||||||
// 에이전트 사고 과정 — 접기 가능
|
|
||||||
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 등)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
flushTask();
|
||||||
return msgs;
|
return msgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function simpleMarkdown(text) {
|
function simpleMarkdown(text) {
|
||||||
return text
|
return text
|
||||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre class="md-code"><code>$2</code></pre>')
|
||||||
.replace(/\n/g, '<br>')
|
.replace(/\n/g, '<br>')
|
||||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||||
.replace(/^## (.*)/gm, '<h3>$1</h3>')
|
.replace(/^## (.*)/gm, '<h3>$1</h3>')
|
||||||
.replace(/^### (.*)/gm, '<h4>$1</h4>')
|
.replace(/^### (.*)/gm, '<h4>$1</h4>')
|
||||||
.replace(/\| (.+?) \|/g, (m) => `<span style="font-family:monospace">${m}</span>`);
|
.replace(/^\- (.*)/gm, '<li>$1</li>')
|
||||||
|
.replace(/^\d+\. (.*)/gm, '<li>$1</li>');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 서버 메시지 핸들러 ───────────────────────────────
|
// ─── 서버 메시지 핸들러 ───────────────────────────────
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class ChatPanel {
|
|||||||
|
|
||||||
const toggle = document.createElement('span');
|
const toggle = document.createElement('span');
|
||||||
toggle.className = 'msg-toggle';
|
toggle.className = 'msg-toggle';
|
||||||
toggle.textContent = msg.collapsed ? '▸' : '▾';
|
toggle.textContent = '▾';
|
||||||
|
|
||||||
const title = document.createElement('span');
|
const title = document.createElement('span');
|
||||||
title.className = 'msg-card-title';
|
title.className = 'msg-card-title';
|
||||||
@@ -131,6 +131,16 @@ class ChatPanel {
|
|||||||
header.appendChild(toggle);
|
header.appendChild(toggle);
|
||||||
header.appendChild(title);
|
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) {
|
if (msg.summary) {
|
||||||
const summary = document.createElement('div');
|
const summary = document.createElement('div');
|
||||||
@@ -141,26 +151,32 @@ class ChatPanel {
|
|||||||
|
|
||||||
card.appendChild(header);
|
card.appendChild(header);
|
||||||
|
|
||||||
// 하위 항목
|
// 도구 호출 목록 (tools)
|
||||||
if (msg.steps && msg.steps.length > 0) {
|
const toolList = msg.tools || msg.steps || [];
|
||||||
|
if (toolList.length > 0) {
|
||||||
const body = document.createElement('div');
|
const body = document.createElement('div');
|
||||||
body.className = 'msg-card-body';
|
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');
|
const row = document.createElement('div');
|
||||||
row.className = 'msg-step';
|
row.className = 'msg-step';
|
||||||
|
|
||||||
const icon = document.createElement('span');
|
const icon = document.createElement('span');
|
||||||
icon.className = 'msg-step-icon';
|
icon.className = 'msg-step-icon';
|
||||||
icon.textContent = step.icon || '•';
|
icon.textContent = tool.icon || '•';
|
||||||
|
|
||||||
const text = document.createElement('span');
|
const text = document.createElement('span');
|
||||||
text.className = 'msg-step-text';
|
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(icon);
|
||||||
row.appendChild(text);
|
row.appendChild(text);
|
||||||
|
row.appendChild(check);
|
||||||
body.appendChild(row);
|
body.appendChild(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +191,7 @@ class ChatPanel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 카드 내부 액션 버튼 (Cancel, Review Changes 등)
|
// 카드 내부 액션 버튼
|
||||||
if (msg.actions && msg.actions.length > 0) {
|
if (msg.actions && msg.actions.length > 0) {
|
||||||
const actionsDiv = document.createElement('div');
|
const actionsDiv = document.createElement('div');
|
||||||
actionsDiv.className = 'msg-card-actions';
|
actionsDiv.className = 'msg-card-actions';
|
||||||
@@ -192,7 +208,7 @@ class ChatPanel {
|
|||||||
if (btn.x && btn.y) {
|
if (btn.x && btn.y) {
|
||||||
el.style.cursor = 'pointer';
|
el.style.cursor = 'pointer';
|
||||||
el.addEventListener('click', (e) => {
|
el.addEventListener('click', (e) => {
|
||||||
e.stopPropagation(); // 카드 토글 방지
|
e.stopPropagation();
|
||||||
if (this.onActionClick) {
|
if (this.onActionClick) {
|
||||||
this.onActionClick({ label: btn.label, x: btn.x, y: btn.y });
|
this.onActionClick({ label: btn.label, x: btn.x, y: btn.y });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user