feat: kakao_send_message 에이전트 도구화 + 멀티기기 메인챗 세션 동기화 + 터미널 전체화면
- src/tools/kakao.ts: 기존 KakaoNotify를 kakao_send_message 도구로 노출, 코드에디터에서는 차단 - app.js/server-v2.ts: 메인챗 마지막 활성 세션을 서버에 기록해 새 기기에서 열 때 자동 이어보기 - code.html/code.js/styles.css: 서버 터미널 패널 전체화면 토글 (Esc로 종료) - companion.py: .vbs 자동시작 시 cwd가 System32로 잡히는 문제 방지 위해 홈 디렉터리로 고정 - papa 워크스페이스: 우즈베크어 학습 진행상황, 서버 이름(지서버/클로서버) 메모 갱신 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -386,7 +386,9 @@ function loadChatSessions() {
|
||||
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;
|
||||
@@ -400,6 +402,41 @@ function loadChatSessions() {
|
||||
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() {
|
||||
@@ -470,6 +507,7 @@ function newChatSession() {
|
||||
activeChatSessionId = id;
|
||||
setAgentSessionId(id);
|
||||
syncActiveChat();
|
||||
updateMainSessionPointer(id);
|
||||
}
|
||||
|
||||
function toggleSessionsEditMode() {
|
||||
@@ -497,6 +535,7 @@ function deleteChatSession(id, ev) {
|
||||
|
||||
saveChatSessions();
|
||||
syncActiveChat();
|
||||
updateMainSessionPointer(activeChatSessionId);
|
||||
// 서버 세션 파일도 삭제 (재로드 시 재등장 방지)
|
||||
api(`/api/chat/sessions/${id}`, { method: 'DELETE' }).catch(() => {});
|
||||
}
|
||||
@@ -504,6 +543,7 @@ function deleteChatSession(id, ev) {
|
||||
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 로드 후 렌더
|
||||
|
||||
@@ -542,6 +542,7 @@
|
||||
<button onclick="addCodeTermShell()" title="서버 터미널 탭 추가" style="background:none;border:none;color:#6b7280;cursor:pointer;font-size:11px;padding:0 6px;line-height:1;flex-shrink:0;font-family:'IBM Plex Mono',monospace">☁️</button>
|
||||
<button id="code-term-ai-error-btn" onclick="_sendActiveTermErrorToAI()" title="터미널 출력을 AI에게 전송" style="background:#1a3a28;color:#22c55e;border:1px solid #2a5a3a;border-radius:4px;padding:1px 7px;font-size:10px;font-weight:700;cursor:pointer;flex-shrink:0;margin-right:2px">🤖</button>
|
||||
<button onclick="codeTermKill()" title="실행 중단 (Ctrl+C)" style="background:none;border:none;color:#f87171;cursor:pointer;font-size:13px;padding:0 6px;line-height:1;flex-shrink:0">⏹</button>
|
||||
<button id="code-term-fullscreen-btn" onclick="toggleCodeTermFullscreen()" title="전체화면" style="background:none;border:none;color:#8b8b8b;cursor:pointer;font-size:13px;padding:0 6px;line-height:1;flex-shrink:0">⛶</button>
|
||||
<button onclick="toggleCodeTerminal()" title="닫기" style="background:none;border:none;color:#3d6b4f;cursor:pointer;font-size:14px;padding:0 8px;line-height:1;flex-shrink:0">×</button>
|
||||
</div>
|
||||
<div id="code-term-container" style="flex:1;min-height:0;position:relative;overflow:hidden"></div>
|
||||
|
||||
@@ -775,6 +775,10 @@ function createXtermShell(container, shellId) {
|
||||
toggleCodeTerminal();
|
||||
return false;
|
||||
}
|
||||
if (e.type === 'keydown' && e.key === 'Escape' && codeTermFullscreen) {
|
||||
toggleCodeTermFullscreen();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const inst = { term, fitAddon, ptyReady: false };
|
||||
@@ -893,6 +897,23 @@ function toggleCodeTerminal() {
|
||||
}
|
||||
if (monacoEditor) setTimeout(() => monacoEditor.layout(), 60);
|
||||
}
|
||||
let codeTermFullscreen = false;
|
||||
function toggleCodeTermFullscreen() {
|
||||
const panel = document.getElementById('code-term-panel');
|
||||
const btn = document.getElementById('code-term-fullscreen-btn');
|
||||
if (!panel) return;
|
||||
codeTermFullscreen = !codeTermFullscreen;
|
||||
panel.classList.toggle('code-term-fullscreen', codeTermFullscreen);
|
||||
if (btn) { btn.classList.toggle('active', codeTermFullscreen); btn.title = codeTermFullscreen ? '전체화면 종료 (Esc)' : '전체화면'; }
|
||||
setTimeout(() => {
|
||||
const s = codeTermShells.find(s => s.id === activeCodeTermShellId);
|
||||
if (s?.fitAddon) s.fitAddon.fit();
|
||||
if (s?.term) s.term.focus();
|
||||
}, 60);
|
||||
}
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && codeTermFullscreen) toggleCodeTermFullscreen();
|
||||
});
|
||||
// ── Browser Preview Panel ────────────────────────────────────────────────────
|
||||
let codePreviewOpen = false;
|
||||
function toggleCodePreview() {
|
||||
|
||||
@@ -37,6 +37,14 @@ HOST = '127.0.0.1'
|
||||
PORT = 9000
|
||||
IS_WINDOWS = sys.platform == 'win32'
|
||||
|
||||
# 자동시작(.vbs)으로 실행되면 프로세스 cwd가 System32로 잡히는 경우가 있어
|
||||
# (wscript.exe 기본 작업 폴더 상속), 터미널이 엉뚱한 위치에서 열리는 걸 막기
|
||||
# 위해 항상 홈 디렉터리에서 시작하도록 고정한다.
|
||||
try:
|
||||
os.chdir(os.path.expanduser('~'))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _refresh_env():
|
||||
"""시작 후 변경된 환경 변수(PATH 등)를 레지스트리에서 실시간으로 반영한다."""
|
||||
|
||||
@@ -2536,6 +2536,16 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#code-term-panel.code-term-fullscreen {
|
||||
position: fixed !important;
|
||||
inset: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
z-index: 2000;
|
||||
border-top: none !important;
|
||||
}
|
||||
#code-term-panel.code-term-fullscreen #code-term-drag { display: none; }
|
||||
#code-term-fullscreen-btn.active { color: #8b5cf6 !important; }
|
||||
.code-ai-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 9px 12px; border-bottom: 1px solid #1a3a28;
|
||||
|
||||
Reference in New Issue
Block a user