feat(history): full conversation trajectory — GetCascadeTrajectory RPC, all step types rendered

This commit is contained in:
2026-03-08 00:25:33 +09:00
parent 215409535b
commit 410d77537e
3 changed files with 109 additions and 35 deletions

View File

@@ -142,58 +142,113 @@
sessionPanel.setActive(sessionId);
const session = bridgeSessions.find(s => s.id === sessionId);
if (session) {
chatPanel.showSession({ name: session.name, status: 'connected' });
chatPanel.showSession({ name: session.name, status: session.isRunning ? 'running' : 'connected' });
}
// cascades에서 대화 내용 가져오기
// trajectory에서 전체 대화 내용 가져오기
try {
const res = await fetch('/api/bridge/cascades');
const res = await fetch(`/api/bridge/trajectory/${sessionId}`);
if (!res.ok) return;
const data = await res.json();
const cascadeData = data.cascades || data;
// 선택된 세션의 데이터 찾기
const cascade = cascadeData[sessionId];
if (cascade) {
const messages = parseCascadeToMessages(cascade);
if (data.trajectory?.steps) {
const messages = parseTrajectoryToMessages(data.trajectory.steps);
chatPanel.updateChat(messages);
}
} catch (e) {
console.warn('[Bridge] cascade 로드 실패:', e);
console.warn('[Bridge] trajectory 로드 실패:', e);
}
}
/**
* Cascade 데이터를 ChatPanel이 이해하는 message[] 형식으로 변환
* Trajectory 스텝 배열을 ChatPanel message[] 형식으로 변환
*/
function parseCascadeToMessages(cascade) {
function parseTrajectoryToMessages(steps) {
const msgs = [];
// latestNotifyUserStep — 마지막 AI 응답
if (cascade.latestNotifyUserStep?.step?.notifyUser?.notificationContent) {
msgs.push({
type: 'text',
html: cascade.latestNotifyUserStep.step.notifyUser.notificationContent
.replace(/\n/g, '<br>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/`([^`]+)`/g, '<code>$1</code>'),
});
}
// latestTaskBoundaryStep — 현재 작업 상태
if (cascade.latestTaskBoundaryStep?.step?.taskBoundary) {
const tb = cascade.latestTaskBoundaryStep.step.taskBoundary;
msgs.push({
type: 'task',
title: tb.taskName || '',
summary: tb.taskSummary || '',
status: tb.taskStatus || '',
mode: tb.mode || '',
});
}
// summary
if (cascade.summary && msgs.length === 0) {
msgs.push({ type: 'text', html: `<p>${cascade.summary}</p>` });
for (const step of steps) {
switch (step.type) {
case 'CORTEX_STEP_TYPE_USER_INPUT': {
const text = step.userInput?.items?.[0]?.chunk?.value || '';
if (text) {
msgs.push({ type: 'user', text });
}
break;
}
case 'CORTEX_STEP_TYPE_NOTIFY_USER': {
const content = step.notifyUser?.notificationContent || '';
if (content) {
msgs.push({
type: 'text',
html: simpleMarkdown(content),
});
}
break;
}
case 'CORTEX_STEP_TYPE_TASK_BOUNDARY': {
const tb = step.taskBoundary || {};
if (tb.taskName) {
msgs.push({
type: 'task',
title: tb.taskName || '',
summary: tb.taskSummary || '',
status: tb.taskStatus || '',
mode: tb.mode || '',
});
}
break;
}
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 || '코드 수정'}`,
});
}
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' ? '완료' : '실행 중'})`,
});
}
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 등)
}
}
return msgs;
}
function simpleMarkdown(text) {
return text
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/\n/g, '<br>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/^## (.*)/gm, '<h3>$1</h3>')
.replace(/^### (.*)/gm, '<h4>$1</h4>')
.replace(/\| (.+?) \|/g, (m) => `<span style="font-family:monospace">${m}</span>`);
}
// ─── 서버 메시지 핸들러 ───────────────────────────────
function handleMessage(msg) {
switch (msg.type) {

View File

@@ -46,6 +46,16 @@ class BridgeClient {
return this._get('/api/cascades');
}
/**
* 개별 대화의 전체 스텝/메시지 (GetCascadeTrajectory RPC)
*/
async getTrajectory(cascadeId) {
return this._post('/api/ls/rpc', {
method: 'GetCascadeTrajectory',
payload: { cascadeId },
});
}
/**
* 메시지 전송
*/

View File

@@ -321,6 +321,15 @@ app.post('/api/bridge/accept-terminal', async (req, res) => {
}
});
app.get('/api/bridge/trajectory/:id', async (req, res) => {
try {
const data = await bridge.getTrajectory(req.params.id);
res.json(data);
} catch (e) {
res.status(502).json({ error: e.message });
}
});
// Bridge WS 이벤트 → 프론트엔드 포워딩
bridge.connectWs((msg) => {
broadcastToAll({ type: 'bridge_event', ...msg });