feat(history): full conversation trajectory — GetCascadeTrajectory RPC, all step types rendered
This commit is contained in:
101
public/js/app.js
101
public/js/app.js
@@ -142,43 +142,49 @@
|
|||||||
sessionPanel.setActive(sessionId);
|
sessionPanel.setActive(sessionId);
|
||||||
const session = bridgeSessions.find(s => s.id === sessionId);
|
const session = bridgeSessions.find(s => s.id === sessionId);
|
||||||
if (session) {
|
if (session) {
|
||||||
chatPanel.showSession({ name: session.name, status: 'connected' });
|
chatPanel.showSession({ name: session.name, status: session.isRunning ? 'running' : 'connected' });
|
||||||
}
|
}
|
||||||
// cascades에서 대화 내용 가져오기
|
// trajectory에서 전체 대화 내용 가져오기
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/bridge/cascades');
|
const res = await fetch(`/api/bridge/trajectory/${sessionId}`);
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const cascadeData = data.cascades || data;
|
if (data.trajectory?.steps) {
|
||||||
// 선택된 세션의 데이터 찾기
|
const messages = parseTrajectoryToMessages(data.trajectory.steps);
|
||||||
const cascade = cascadeData[sessionId];
|
|
||||||
if (cascade) {
|
|
||||||
const messages = parseCascadeToMessages(cascade);
|
|
||||||
chatPanel.updateChat(messages);
|
chatPanel.updateChat(messages);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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 = [];
|
const msgs = [];
|
||||||
// latestNotifyUserStep — 마지막 AI 응답
|
for (const step of steps) {
|
||||||
if (cascade.latestNotifyUserStep?.step?.notifyUser?.notificationContent) {
|
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({
|
msgs.push({
|
||||||
type: 'text',
|
type: 'text',
|
||||||
html: cascade.latestNotifyUserStep.step.notifyUser.notificationContent
|
html: simpleMarkdown(content),
|
||||||
.replace(/\n/g, '<br>')
|
|
||||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
||||||
.replace(/`([^`]+)`/g, '<code>$1</code>'),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// latestTaskBoundaryStep — 현재 작업 상태
|
break;
|
||||||
if (cascade.latestTaskBoundaryStep?.step?.taskBoundary) {
|
}
|
||||||
const tb = cascade.latestTaskBoundaryStep.step.taskBoundary;
|
case 'CORTEX_STEP_TYPE_TASK_BOUNDARY': {
|
||||||
|
const tb = step.taskBoundary || {};
|
||||||
|
if (tb.taskName) {
|
||||||
msgs.push({
|
msgs.push({
|
||||||
type: 'task',
|
type: 'task',
|
||||||
title: tb.taskName || '',
|
title: tb.taskName || '',
|
||||||
@@ -187,13 +193,62 @@
|
|||||||
mode: tb.mode || '',
|
mode: tb.mode || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// summary
|
break;
|
||||||
if (cascade.summary && msgs.length === 0) {
|
}
|
||||||
msgs.push({ type: 'text', html: `<p>${cascade.summary}</p>` });
|
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;
|
return msgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function simpleMarkdown(text) {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
.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) {
|
function handleMessage(msg) {
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
|
|||||||
@@ -46,6 +46,16 @@ class BridgeClient {
|
|||||||
return this._get('/api/cascades');
|
return this._get('/api/cascades');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개별 대화의 전체 스텝/메시지 (GetCascadeTrajectory RPC)
|
||||||
|
*/
|
||||||
|
async getTrajectory(cascadeId) {
|
||||||
|
return this._post('/api/ls/rpc', {
|
||||||
|
method: 'GetCascadeTrajectory',
|
||||||
|
payload: { cascadeId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메시지 전송
|
* 메시지 전송
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 WS 이벤트 → 프론트엔드 포워딩
|
||||||
bridge.connectWs((msg) => {
|
bridge.connectWs((msg) => {
|
||||||
broadcastToAll({ type: 'bridge_event', ...msg });
|
broadcastToAll({ type: 'bridge_event', ...msg });
|
||||||
|
|||||||
Reference in New Issue
Block a user