// chat-sessions.js — 채팅 세션 저장/로드/전환/삭제 (sidebar 세션 목록). // extracted from app.js (v4.3.19-3 split). function saveChatSessions() { localStorage.setItem(chatSessionsKey(), JSON.stringify(chatSessions)); } function makeSessionTitle(history) { const firstUser = (history || []).find(m => m.role === 'user'); if (!firstUser) return 'New chat'; // Strip markdown image/link syntax for clean session title let text = firstUser.content .replace(/!\[[^\]]*\]\([^)]+\)/g, '[image]') .replace(/\[[^\]]+\]\([^)]+\)/g, (m) => m.replace(/\[([^\]]*)\].*/, '$1')) .replace(/```[\s\S]*?```/g, '') .replace(/\n/g, ' ') .trim(); return text.slice(0, 42) || 'New chat'; } function loadChatSessions() { const key = chatSessionsKey(); try { const raw = localStorage.getItem(key); if (raw) { chatSessions = JSON.parse(raw); } else { // Migrate from old non-namespaced key once, then delete it so other users can't see it. const oldRaw = localStorage.getItem(CHAT_SESSIONS_KEY_BASE); if (oldRaw) { chatSessions = JSON.parse(oldRaw); localStorage.setItem(key, oldRaw); localStorage.removeItem(CHAT_SESSIONS_KEY_BASE); } else { chatSessions = []; } } } catch { chatSessions = []; } if (!Array.isArray(chatSessions)) chatSessions = []; let isFreshDevice = false; if (chatSessions.length === 0) { isFreshDevice = true; const id = crypto?.randomUUID?.() || ('chat_' + Math.random().toString(36).slice(2)); chatSessions.push({ id, title: 'New chat', history: [], processLog: [], createdAt: Date.now(), updatedAt: Date.now() }); activeChatSessionId = id; setAgentSessionId(id); saveChatSessions(); } else if (!activeChatSessionId || !chatSessions.some(s => s.id === activeChatSessionId)) { activeChatSessionId = chatSessions[0].id; setAgentSessionId(activeChatSessionId); } if (!agentSessionId || agentSessionId !== activeChatSessionId) setAgentSessionId(activeChatSessionId); syncActiveChat(); // 서버 세션 목록과 비교해 누락된 세션을 사이드바에 추가 syncServerSessions(); // 새 기기에서 처음 열었다면, 다른 기기에서 이어가던 대화로 자동 복귀 if (isFreshDevice) resumeMainSessionPointer(); } // 메인챗의 "마지막 활성 세션"을 서버에 기록 — 다른 기기에서 새로 열었을 때 이어보기용 function updateMainSessionPointer(id) { if (!id) return; api('/api/user/app-sessions', { method: 'PUT', body: JSON.stringify({ appKey: 'main', sessionId: id }) }).catch(() => {}); } async function resumeMainSessionPointer() { try { const d = await api('/api/user/app-sessions'); const ptr = d && d.main; if (!ptr) { updateMainSessionPointer(activeChatSessionId); return; } // 최초 로그인: 이 세션을 기준점으로 등록 if (ptr === activeChatSessionId) return; const cur = chatSessions.find(s => s.id === activeChatSessionId); if (cur && cur.history && cur.history.length > 0) return; // 이미 이 기기에서 대화가 시작됐으면 건드리지 않음 const hd = await api('/api/chat/sessions/' + encodeURIComponent(ptr)); if (!hd || !hd.success || !Array.isArray(hd.history) || hd.history.length === 0) return; // 비어있던 자리표시자 세션은 제거하고 포인터가 가리키는 세션으로 전환 chatSessions = chatSessions.filter(s => s.id !== activeChatSessionId); let existing = chatSessions.find(s => s.id === ptr); if (!existing) { existing = { id: ptr, title: makeSessionTitle(hd.history), history: hd.history, processLog: [], createdAt: hd.createdAt || Date.now(), updatedAt: hd.lastActiveAt || Date.now() }; chatSessions.unshift(existing); } else { existing.history = hd.history; delete existing.serverOnly; } activeChatSessionId = ptr; setAgentSessionId(ptr); saveChatSessions(); syncActiveChat(); } catch { /* 서버 미응답 시 무시 */ } } async function syncServerSessions() { try { const res = await fetch('/api/chat/sessions'); if (!res.ok) return; const data = await res.json(); if (!data.success || !Array.isArray(data.sessions)) return; let changed = false; for (const srv of data.sessions) { const existing = chatSessions.find(s => s.id === srv.id); if (!existing) { // 서버에만 있는 세션 → 스텁으로 추가 (history는 열 때 로드) chatSessions.push({ id: srv.id, title: srv.title || 'New chat', history: [], processLog: [], createdAt: srv.createdAt, updatedAt: srv.lastActiveAt, serverOnly: true, // 열 때 서버에서 history 로드 }); changed = true; } else if (existing.serverOnly) { // 이미 스텁으로 있으면 메타만 업데이트 existing.title = srv.title || existing.title; existing.updatedAt = srv.lastActiveAt; changed = true; } } if (changed) { saveChatSessions(); renderSessionsList(); } } catch { /* 서버 미응답 시 무시 */ } } function syncActiveChat() { const sess = chatSessions.find(s => s.id === activeChatSessionId); chatHistory = sess ? (sess.history || []) : []; processLogEntries = sess ? (sess.processLog || []) : []; liveTaskSteps = []; // clear live steps so a different session never inherits in-progress tool display // Mark session as read when opening it if (sess) sess.unread = false; renderChatMessages(); renderProcessLog(); renderLiveStepsPanel(); renderSessionsList(); updateStats([]); loadAgentSessionContext(); } function persistActiveChat() { const idx = chatSessions.findIndex(s => s.id === activeChatSessionId); if (idx === -1) return; chatSessions[idx].history = chatHistory; chatSessions[idx].processLog = processLogEntries; chatSessions[idx].title = makeSessionTitle(chatHistory); chatSessions[idx].updatedAt = Date.now(); saveChatSessions(); renderSessionsList(); updateStats([]); } function newChatSession() { const id = crypto?.randomUUID?.() || ('chat_' + Math.random().toString(36).slice(2)); chatSessions.unshift({ id, title: 'New chat', history: [], processLog: [], createdAt: Date.now(), updatedAt: Date.now() }); activeChatSessionId = id; setAgentSessionId(id); syncActiveChat(); updateMainSessionPointer(id); } function toggleSessionsEditMode() { sessionsEditMode = !sessionsEditMode; const btn = document.getElementById('sessions-edit-btn'); if (btn) btn.textContent = sessionsEditMode ? '완료' : '편집'; renderSessionsList(); } function deleteChatSession(id, ev) { if (ev) ev.stopPropagation(); const idx = chatSessions.findIndex(s => s.id === id); if (idx === -1) return; chatSessions.splice(idx, 1); if (chatSessions.length === 0) { const newId = crypto?.randomUUID?.() || ('chat_' + Math.random().toString(36).slice(2)); chatSessions.push({ id: newId, title: 'New chat', history: [], processLog: [], createdAt: Date.now(), updatedAt: Date.now() }); activeChatSessionId = newId; setAgentSessionId(newId); } else if (activeChatSessionId === id) { activeChatSessionId = chatSessions[0].id; setAgentSessionId(activeChatSessionId); } saveChatSessions(); syncActiveChat(); updateMainSessionPointer(activeChatSessionId); // 서버 세션 파일도 삭제 (재로드 시 재등장 방지) api(`/api/chat/sessions/${id}`, { method: 'DELETE' }).catch(() => {}); } function openSession(id) { activeChatSessionId = id; setAgentSessionId(id); updateMainSessionPointer(id); const sess = chatSessions.find(s => s.id === id); if (sess && sess.serverOnly && sess.history.length === 0) { // 서버에서 history 로드 후 렌더 fetch(`/api/chat/sessions/${id}`) .then(r => r.json()) .then(data => { if (data.success && Array.isArray(data.history)) { sess.history = data.history; sess.title = makeSessionTitle(data.history) || sess.title; delete sess.serverOnly; saveChatSessions(); syncActiveChat(); renderSessionsList(); } }) .catch(() => syncActiveChat()); renderSessionsList(); // 즉시 세션 활성화 표시 return; } syncActiveChat(); } function markSessionUnread(sessionId) { const sess = chatSessions.find(s => s.id === sessionId); if (sess && sessionId !== activeChatSessionId) { sess.unread = true; renderSessionsList(); } } // ---- Mode switching ---- function setMode(mode) { if (!['chat','bgtasks'].includes(mode)) mode = 'chat'; currentMode = mode; document.getElementById('chat-view').style.display = mode === 'chat' ? 'flex' : 'none'; const bgtView = document.getElementById('bgtasks-view'); if (bgtView) bgtView.style.display = mode === 'bgtasks' ? 'flex' : 'none'; const aside = document.querySelector('aside'); const rightPanel = document.getElementById('right-panel'); if (aside) aside.style.display = ''; if (rightPanel) rightPanel.style.display = mode === 'chat' ? '' : 'none'; // sidebar tab 하이라이트 동기화 document.getElementById('tab-bgtasks')?.classList.toggle('active', mode === 'bgtasks'); if (mode === 'bgtasks') refreshBgTasks(); }