From db70840b7aec3e8406b9fe919be7f31a3074c8bb Mon Sep 17 00:00:00 2001 From: Variet Date: Sun, 8 Mar 2026 00:11:29 +0900 Subject: [PATCH] =?UTF-8?q?feat(ui):=20session=20identification=20?= =?UTF-8?q?=E2=80=94=20project=20badges,=20relative=20time,=20running=20in?= =?UTF-8?q?dicator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/css/style.css | 38 ++++++++++++++++++++++++++++++ public/js/app.js | 48 +++++++++++++++++++++++++++----------- public/js/session-panel.js | 44 ++++++++++++++++++++++++++++++---- 3 files changed, 112 insertions(+), 18 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index 2b00ce5..d3eb44f 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -259,6 +259,44 @@ body { font-size: 11px; color: var(--text-muted); margin-top: 2px; + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; +} + +.session-project { + display: inline-block; + font-size: 9px; + padding: 1px 6px; + border-radius: 8px; + color: #fff; + font-weight: 600; + letter-spacing: 0.3px; + line-height: 1.4; +} + +.running-dot { + color: #22c55e; + font-size: 10px; + margin-right: 3px; + animation: pulse 1.5s ease-in-out infinite; +} + +.session-card.is-running { + border-left: 2px solid #22c55e; +} + +@keyframes pulse { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.3; + } } .session-remove { diff --git a/public/js/app.js b/public/js/app.js index cb4171d..96e25cd 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -92,19 +92,41 @@ 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, - })); + // 세션 목록 + cascades를 병렬로 가져오기 + const [sessRes, cascRes] = await Promise.all([ + fetch('/api/bridge/sessions'), + fetch('/api/bridge/cascades'), + ]); + if (!sessRes.ok) return; + const sessData = await sessRes.json(); + let cascadeMap = {}; + if (cascRes.ok) { + const cascData = await cascRes.json(); + cascadeMap = cascData.cascades || cascData; + } + + bridgeSessions = (sessData.sessions || []).map(s => { + const cascade = cascadeMap[s.id] || {}; + // 워크스페이스에서 프로젝트명 추출 + const wsUri = cascade.workspaces?.[0]?.workspaceFolderAbsoluteUri || ''; + const project = wsUri ? decodeURIComponent(wsUri.split('/').pop()) : ''; + // 상태 + const runStatus = cascade.status || ''; + const isRunning = runStatus.includes('RUNNING'); + return { + id: s.id, + name: s.title || s.id.substring(0, 8), + host: 'bridge', + cdpPort: 0, + status: isRunning ? 'running' : 'connected', + title: s.title, + stepCount: s.stepCount, + lastModified: s.lastModifiedTime, + project: project, + isRunning: isRunning, + isBridge: true, + }; + }); sessionPanel.update(bridgeSessions); // 활성 세션 없으면 가장 최근 대화 자동 선택 if (!sessionPanel.activeSessionId && bridgeSessions.length > 0) { diff --git a/public/js/session-panel.js b/public/js/session-panel.js index 934f23d..53424f2 100644 --- a/public/js/session-panel.js +++ b/public/js/session-panel.js @@ -43,17 +43,28 @@ class SessionPanel { return; } - this.listEl.innerHTML = this.sessions.map(s => ` -
{ + const timeStr = s.lastModified ? this._relativeTime(s.lastModified) : ''; + const projectBadge = s.project + ? `${this._escapeHtml(s.project)}` + : ''; + const runningDot = s.isRunning ? '' : ''; + const detail = s.isBridge + ? `${s.stepCount || 0} steps${timeStr ? ' · ' + timeStr : ''}` + : `${s.host}:${s.cdpPort} · ${this._statusText(s.status)}`; + + return ` +
-
${this._escapeHtml(s.name)}
-
${s.isBridge ? `${s.stepCount || 0} steps · 대화` : `${s.host}:${s.cdpPort} · ${this._statusText(s.status)}`}
+
${runningDot}${this._escapeHtml(s.name)}
+
${projectBadge}${detail}
- `).join(''); + `; + }).join(''); // 이벤트 바인딩 this.listEl.querySelectorAll('.session-card').forEach(card => { @@ -78,11 +89,34 @@ class SessionPanel { connected: '연결됨', connecting: '연결 중...', disconnected: '연결 끊김', + running: '실행 중', error: '오류', }; return map[status] || status; } + _relativeTime(iso) { + const diff = Date.now() - new Date(iso).getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return '방금'; + if (mins < 60) return `${mins}분 전`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}시간 전`; + const days = Math.floor(hrs / 24); + return `${days}일 전`; + } + + _projectColor(name) { + const colors = { + 'gravity_web': '#6366f1', + 'Deriva': '#f59e0b', + 'MMF': '#10b981', + 'test1': '#8b5cf6', + 'test2': '#ec4899', + }; + return colors[name] || '#64748b'; + } + _escapeHtml(str) { const div = document.createElement('div'); div.textContent = str;