feat: claude-app 로컬 LLM 코딩 에이전트 추가 + 코드 에디터 임베드

- Ollama/LM Studio 기반 터미널형 코딩 에이전트(claude-app.html/js): 테마 11종,
  Plan/Confirm/Auto/Yolo 실행 모드, companion.py 연동
- companion.py에 browse_folder/fs_list/fs_read/fs_write 추가 — 네이티브 폴더
  선택이 곧 Bash 작업 경로가 되도록 통합해 Write/Bash가 서로 다른 폴더를 보던
  문제 해결
- code.html에 클로드 앱 하단 패널 추가, 우측 AI 채팅 패널은 그대로 유지되도록
  그리드 레이아웃 조정, 패널 높이를 전체 높이의 50~70%로 제한
- iframe 관련 버그 수정: src="" 판정 오류로 패널이 로드되지 않던 문제,
  드래그 리사이즈 중 마우스가 iframe 위를 지나면 릴리즈가 안 되던 문제
  (미리보기/에뮬레이터 패널도 동일 수정)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
kim
2026-07-03 23:41:07 +09:00
co-authored by Claude Sonnet 5
parent d953f69de4
commit 5fdf99fb73
7 changed files with 1379 additions and 6 deletions
+68 -5
View File
@@ -904,7 +904,7 @@ function toggleCodePreview() {
const saved = localStorage.getItem('code_preview_url');
if (saved && urlInput) urlInput.value = saved;
const iframe = document.getElementById('code-preview-iframe');
if (iframe && !iframe.src) iframe.src = (urlInput ? urlInput.value : 'http://127.0.0.1:8000');
if (iframe && !iframe.getAttribute('src')) iframe.src = (urlInput ? urlInput.value : 'http://127.0.0.1:8000');
} else {
panel.style.display = 'none';
if (btn) btn.classList.remove('active');
@@ -950,6 +950,57 @@ function emuPanelNewTab() {
if (_emuPanelUrl) window.open(_emuPanelUrl, '_blank');
}
// 클로드 앱 패널 높이는 항상 전체 높이의 1/2~7/10 사이로 제한한다.
function _claudePanelBounds() {
const total = document.getElementById('code-view')?.clientHeight || window.innerHeight;
return { min: total * 0.5, max: total * 0.7 };
}
let _claudePanelOpen = false;
function toggleClaudeAppPanel() {
const panel = document.getElementById('code-claude-panel');
const btn = document.getElementById('code-claude-btn');
if (!panel) return;
_claudePanelOpen = !_claudePanelOpen;
panel.style.display = _claudePanelOpen ? 'flex' : 'none';
if (btn) btn.classList.toggle('active', _claudePanelOpen);
if (_claudePanelOpen) {
const iframe = document.getElementById('code-claude-iframe');
if (iframe && !iframe.getAttribute('src')) iframe.src = '/claude-app.html';
const { min, max } = _claudePanelBounds();
const cur = panel.offsetHeight;
if (cur < min || cur > max) panel.style.height = Math.round((min + max) / 2) + 'px';
}
if (monacoEditor) setTimeout(() => monacoEditor.layout(), 60);
}
// Claude App panel resize drag
document.addEventListener('DOMContentLoaded', function() {
const drag = document.getElementById('code-claude-drag');
const panel = document.getElementById('code-claude-panel');
if (!drag || !panel) return;
let startY = 0, startH = 0;
drag.addEventListener('mouseover', () => drag.style.background = 'rgba(189,147,249,.4)');
drag.addEventListener('mousedown', e => {
startY = e.clientY; startH = panel.offsetHeight; e.preventDefault();
const { min, max } = _claudePanelBounds();
// 드래그 중 마우스가 iframe 위로 지나가면 부모 document가 mousemove/mouseup을 못 받아
// 드래그가 안 풀리므로, 드래그하는 동안은 iframe이 마우스 이벤트를 가로채지 않게 한다.
const iframe = document.getElementById('code-claude-iframe');
if (iframe) iframe.style.pointerEvents = 'none';
const onMove = e2 => {
const dy = startY - e2.clientY;
panel.style.height = Math.max(min, Math.min(max, startH + dy)) + 'px';
};
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
drag.style.background = '';
if (iframe) iframe.style.pointerEvents = '';
};
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
});
});
function openArduinoEmulator() {
const file = codeFiles.find(f => f.id === activeCodeFileId);
const btn = document.getElementById('code-emu-btn');
@@ -1090,14 +1141,20 @@ document.addEventListener('DOMContentLoaded', function() {
const panel = document.getElementById('code-preview-panel');
if (!drag || !panel) return;
let startY=0, startH=0;
drag.addEventListener('mouseover', ()=>drag.style.background='rgba(96,165,250,.4)');
drag.addEventListener('mousedown', e => {
startY=e.clientY; startH=panel.offsetHeight; e.preventDefault();
const iframe = document.getElementById('code-preview-iframe');
if (iframe) iframe.style.pointerEvents = 'none';
const onMove = e2 => {
const dy = startY - e2.clientY;
panel.style.height = Math.max(100, Math.min(window.innerHeight*.7, startH+dy)) + 'px';
};
const onUp = () => { document.removeEventListener('mousemove',onMove); document.removeEventListener('mouseup',onUp); drag.style.background=''; };
drag.addEventListener('mouseover', ()=>drag.style.background='rgba(96,165,250,.4)');
const onUp = () => {
document.removeEventListener('mousemove',onMove); document.removeEventListener('mouseup',onUp);
drag.style.background='';
if (iframe) iframe.style.pointerEvents = '';
};
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
});
});
@@ -6466,11 +6523,17 @@ document.addEventListener('DOMContentLoaded', function() {
const panel = document.getElementById('code-emu-panel');
if (!drag || !panel) return;
let startY=0, startH=0;
drag.addEventListener('mouseover', ()=>drag.style.background='rgba(34,197,94,.4)');
drag.addEventListener('mousedown', e => {
startY=e.clientY; startH=panel.offsetHeight; e.preventDefault();
const iframe = document.getElementById('code-emu-iframe');
if (iframe) iframe.style.pointerEvents = 'none';
const onMove = e2 => { const dy=startY-e2.clientY; panel.style.height=Math.max(120,Math.min(window.innerHeight*.75,startH+dy))+'px'; };
const onUp = () => { document.removeEventListener('mousemove',onMove); document.removeEventListener('mouseup',onUp); drag.style.background=''; };
drag.addEventListener('mouseover', ()=>drag.style.background='rgba(34,197,94,.4)');
const onUp = () => {
document.removeEventListener('mousemove',onMove); document.removeEventListener('mouseup',onUp);
drag.style.background='';
if (iframe) iframe.style.pointerEvents = '';
};
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
});
});