feat(ui): session identification — project badges, relative time, running indicator
This commit is contained in:
@@ -259,6 +259,44 @@ body {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-top: 2px;
|
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 {
|
.session-remove {
|
||||||
|
|||||||
@@ -92,19 +92,41 @@
|
|||||||
|
|
||||||
async function loadBridgeSessions() {
|
async function loadBridgeSessions() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/bridge/sessions');
|
// 세션 목록 + cascades를 병렬로 가져오기
|
||||||
if (!res.ok) return;
|
const [sessRes, cascRes] = await Promise.all([
|
||||||
const data = await res.json();
|
fetch('/api/bridge/sessions'),
|
||||||
bridgeSessions = (data.sessions || []).map(s => ({
|
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,
|
id: s.id,
|
||||||
name: s.title || s.id.substring(0, 8),
|
name: s.title || s.id.substring(0, 8),
|
||||||
host: 'bridge',
|
host: 'bridge',
|
||||||
cdpPort: 0,
|
cdpPort: 0,
|
||||||
status: 'connected',
|
status: isRunning ? 'running' : 'connected',
|
||||||
title: s.title,
|
title: s.title,
|
||||||
stepCount: s.stepCount,
|
stepCount: s.stepCount,
|
||||||
|
lastModified: s.lastModifiedTime,
|
||||||
|
project: project,
|
||||||
|
isRunning: isRunning,
|
||||||
isBridge: true,
|
isBridge: true,
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
sessionPanel.update(bridgeSessions);
|
sessionPanel.update(bridgeSessions);
|
||||||
// 활성 세션 없으면 가장 최근 대화 자동 선택
|
// 활성 세션 없으면 가장 최근 대화 자동 선택
|
||||||
if (!sessionPanel.activeSessionId && bridgeSessions.length > 0) {
|
if (!sessionPanel.activeSessionId && bridgeSessions.length > 0) {
|
||||||
|
|||||||
@@ -43,17 +43,28 @@ class SessionPanel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listEl.innerHTML = this.sessions.map(s => `
|
this.listEl.innerHTML = this.sessions.map(s => {
|
||||||
<div class="session-card ${s.id === this.activeSessionId ? 'active' : ''}"
|
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}">
|
data-session-id="${s.id}">
|
||||||
<div class="session-indicator ${s.status}"></div>
|
<div class="session-indicator ${s.status}"></div>
|
||||||
<div class="session-info">
|
<div class="session-info">
|
||||||
<div class="session-name">${this._escapeHtml(s.name)}</div>
|
<div class="session-name">${runningDot}${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-detail">${projectBadge}${detail}</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="session-remove" data-remove-id="${s.id}" title="세션 제거">✕</button>
|
<button class="session-remove" data-remove-id="${s.id}" title="세션 제거">✕</button>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
// 이벤트 바인딩
|
// 이벤트 바인딩
|
||||||
this.listEl.querySelectorAll('.session-card').forEach(card => {
|
this.listEl.querySelectorAll('.session-card').forEach(card => {
|
||||||
@@ -78,11 +89,34 @@ class SessionPanel {
|
|||||||
connected: '연결됨',
|
connected: '연결됨',
|
||||||
connecting: '연결 중...',
|
connecting: '연결 중...',
|
||||||
disconnected: '연결 끊김',
|
disconnected: '연결 끊김',
|
||||||
|
running: '실행 중',
|
||||||
error: '오류',
|
error: '오류',
|
||||||
};
|
};
|
||||||
return map[status] || status;
|
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) {
|
_escapeHtml(str) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = str;
|
div.textContent = str;
|
||||||
|
|||||||
Reference in New Issue
Block a user