feat(ui): session identification — project badges, relative time, running indicator

This commit is contained in:
2026-03-08 00:11:29 +09:00
parent 808eb66849
commit db70840b7a
3 changed files with 112 additions and 18 deletions

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -43,17 +43,28 @@ class SessionPanel {
return;
}
this.listEl.innerHTML = this.sessions.map(s => `
<div class="session-card ${s.id === this.activeSessionId ? 'active' : ''}"
this.listEl.innerHTML = this.sessions.map(s => {
const timeStr = s.lastModified ? this._relativeTime(s.lastModified) : '';
const projectBadge = s.project
? `<span class="session-project" style="background:${this._projectColor(s.project)}">${this._escapeHtml(s.project)}</span>`
: '';
const runningDot = s.isRunning ? '<span class="running-dot">●</span>' : '';
const detail = s.isBridge
? `${s.stepCount || 0} steps${timeStr ? ' · ' + timeStr : ''}`
: `${s.host}:${s.cdpPort} · ${this._statusText(s.status)}`;
return `
<div class="session-card ${s.id === this.activeSessionId ? 'active' : ''} ${s.isRunning ? 'is-running' : ''}"
data-session-id="${s.id}">
<div class="session-indicator ${s.status}"></div>
<div class="session-info">
<div class="session-name">${this._escapeHtml(s.name)}</div>
<div class="session-detail">${s.isBridge ? `${s.stepCount || 0} steps · 대화` : `${s.host}:${s.cdpPort} · ${this._statusText(s.status)}`}</div>
<div class="session-name">${runningDot}${this._escapeHtml(s.name)}</div>
<div class="session-detail">${projectBadge}${detail}</div>
</div>
<button class="session-remove" data-remove-id="${s.id}" title="세션 제거">✕</button>
</div>
`).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;