From 808eb66849d871dba5d99ba9a8591c41b2b154d4 Mon Sep 17 00:00:00 2001 From: Variet Date: Sun, 8 Mar 2026 00:02:15 +0900 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20Bridge-based=20UI=20=E2=80=94?= =?UTF-8?q?=20auto-load=20sessions,=20cascade=20content=20display,=20Bridg?= =?UTF-8?q?e=20send?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/app.js | 161 +++++++++++++++++++++++++++++-------- public/js/session-panel.js | 2 +- 2 files changed, 130 insertions(+), 33 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index d4c430f..cb4171d 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -49,6 +49,8 @@ clearTimeout(reconnectTimer); reconnectTimer = null; } + // Bridge 세션 자동 로드 + loadBridgeSessions(); }; ws.onclose = () => { @@ -84,11 +86,97 @@ } } + // ─── Bridge 세션 관리 ────────────────────────────────── + let bridgeSessions = []; // Bridge에서 가져온 대화 목록 + let activeBridgeSession = null; + + async function loadBridgeSessions() { + try { + const res = await fetch('/api/bridge/sessions'); + if (!res.ok) return; + const data = await res.json(); + bridgeSessions = (data.sessions || []).map(s => ({ + id: s.id, + name: s.title || s.id.substring(0, 8), + host: 'bridge', + cdpPort: 0, + status: 'connected', + title: s.title, + stepCount: s.stepCount, + isBridge: true, + })); + sessionPanel.update(bridgeSessions); + // 활성 세션 없으면 가장 최근 대화 자동 선택 + if (!sessionPanel.activeSessionId && bridgeSessions.length > 0) { + selectBridgeSession(bridgeSessions[0].id); + } + } catch (e) { + console.warn('[Bridge] 세션 로드 실패:', e); + } + } + + async function selectBridgeSession(sessionId) { + activeBridgeSession = sessionId; + sessionPanel.setActive(sessionId); + const session = bridgeSessions.find(s => s.id === sessionId); + if (session) { + chatPanel.showSession({ name: session.name, status: 'connected' }); + } + // cascades에서 대화 내용 가져오기 + try { + const res = await fetch('/api/bridge/cascades'); + if (!res.ok) return; + const data = await res.json(); + const cascadeData = data.cascades || data; + // 선택된 세션의 데이터 찾기 + const cascade = cascadeData[sessionId]; + if (cascade) { + const messages = parseCascadeToMessages(cascade); + chatPanel.updateChat(messages); + } + } catch (e) { + console.warn('[Bridge] cascade 로드 실패:', e); + } + } + + /** + * Cascade 데이터를 ChatPanel이 이해하는 message[] 형식으로 변환 + */ + function parseCascadeToMessages(cascade) { + 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}

` }); + } + return msgs; + } + // ─── 서버 메시지 핸들러 ─────────────────────────────── function handleMessage(msg) { switch (msg.type) { case 'sessions_list': - sessionPanel.update(msg.sessions); + // CDP 세션 (레거시) — Bridge 세션과 병합하지 않음 break; case 'session_switched': @@ -108,6 +196,10 @@ } break; + case 'bridge_event': + handleBridgeEvent(msg); + break; + case 'screenshot': screenshotImage.src = `data:image/jpeg;base64,${msg.data}`; screenshotOverlay.style.display = 'flex'; @@ -120,44 +212,35 @@ } break; - case 'screencast_started': - if (!msg.success) { - showToast('Screencast 시작 실패', 'error'); - } - break; - - case 'screencast_stopped': - break; - - case 'action_clicked': - // 서버에서의 확인 응답 - break; - - case 'auto_discovered': - showToast(`Antigravity 자동 감지: ${msg.session.name}`, 'success'); - // 활성 세션이 없으면 자동 전환 - if (!sessionPanel.activeSessionId) { - sendWs({ type: 'switch_session', sessionId: msg.session.id }); - } - break; - - case 'session_lost': - showToast('Antigravity 연결 소실', 'warning'); - if (sessionPanel.activeSessionId === msg.sessionId) { - chatPanel.showEmpty(); - sessionPanel.setActive(null); - } - break; - case 'error': showToast(msg.message, 'error'); break; } } + function handleBridgeEvent(msg) { + switch (msg.type) { + case 'bridge_event': + // step_changed → 활성 대화 갱신 + if (msg.step_changed || msg.sessions) { + loadBridgeSessions(); + } + if (msg.new_conversation) { + loadBridgeSessions(); + } + break; + } + } + // ─── 세션 패널 이벤트 ───────────────────────────────── sessionPanel.onSessionSelect = (sessionId) => { - sendWs({ type: 'switch_session', sessionId }); + // Bridge 세션인지 확인 + const isBridge = bridgeSessions.some(s => s.id === sessionId); + if (isBridge) { + selectBridgeSession(sessionId); + } else { + sendWs({ type: 'switch_session', sessionId }); + } }; sessionPanel.onSessionRemove = async (sessionId) => { @@ -176,7 +259,21 @@ }; // ─── 채팅 패널 이벤트 ───────────────────────────────── - chatPanel.onSendMessage = (text) => { + chatPanel.onSendMessage = async (text) => { + if (!sessionPanel.activeSessionId) return; + // Bridge 세션이면 Bridge API로 전송 + const isBridge = bridgeSessions.some(s => s.id === sessionPanel.activeSessionId); + if (isBridge) { + try { + const res = await fetch('/api/bridge/send', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: text, sessionId: sessionPanel.activeSessionId }), + }); + if (!res.ok) showToast('전송 실패', 'error'); + } catch { showToast('전송 실패', 'error'); } + return; + } sendWs({ type: 'send_message', sessionId: sessionPanel.activeSessionId, diff --git a/public/js/session-panel.js b/public/js/session-panel.js index 7b0446c..934f23d 100644 --- a/public/js/session-panel.js +++ b/public/js/session-panel.js @@ -49,7 +49,7 @@ class SessionPanel {
${this._escapeHtml(s.name)}
-
${s.host}:${s.cdpPort} · ${this._statusText(s.status)}
+
${s.isBridge ? `${s.stepCount || 0} steps · 대화` : `${s.host}:${s.cdpPort} · ${this._statusText(s.status)}`}