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:
kim
2026-07-05 14:41:57 +09:00
co-authored by Claude Sonnet 5
parent ce1a99b723
commit 186dc4cb23
11 changed files with 193 additions and 4 deletions
+40
View File
@@ -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 로드 후 렌더
+1
View File
@@ -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">&times;</button>
</div>
<div id="code-term-container" style="flex:1;min-height:0;position:relative;overflow:hidden"></div>
+21
View File
@@ -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() {
+8
View File
@@ -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 등)를 레지스트리에서 실시간으로 반영한다."""
+10
View File
@@ -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;