diff --git a/public/js/app.js b/public/js/app.js index 96e25cd..0902107 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -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, '
') - .replace(/\*\*(.*?)\*\*/g, '$1') - .replace(/`([^`]+)`/g, '$1'), - }); - } - // 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: `

${cascade.summary}

` }); + 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, '&').replace(//g, '>') + .replace(/\n/g, '
') + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/`([^`]+)`/g, '$1') + .replace(/^## (.*)/gm, '

$1

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

$1

') + .replace(/\| (.+?) \|/g, (m) => `${m}`); + } + // ─── 서버 메시지 핸들러 ─────────────────────────────── function handleMessage(msg) { switch (msg.type) { diff --git a/server/bridge-client.js b/server/bridge-client.js index 6a4e623..52eb974 100644 --- a/server/bridge-client.js +++ b/server/bridge-client.js @@ -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 }, + }); + } + /** * 메시지 전송 */ diff --git a/server/index.js b/server/index.js index 57c9521..bb9d00e 100644 --- a/server/index.js +++ b/server/index.js @@ -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 });