Code editor UI: per-file streaming bubbles, options UI, terminal error button, model card
- Per-file streaming: text shows while AI works, collapses on tool call, ✅완료 at end - Options UI: numbered choice buttons with number/keyword input selection - Number input in chat (1~6) selects corresponding option chip - Terminal error 🤖 button permanently visible in header bar - Model card moved to tabs bar top-right, synced with panel width - Removed 코드 저장 디렉토리 from settings; merged save/folder into sidebar - Removed language selector from chat header - Bottom cards anchored to sidebar bottom - Simplified planInstruction prompt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11241,7 +11241,7 @@ app.post('/api/chat', async (req, res) => {
|
||||
|
||||
// Record whether this code-editor turn uses the browser's local folder. The
|
||||
// server then skips disk file-tool execution (client is source of truth).
|
||||
if (/^(code_ai_|proj-gen-|proj-plan-)/.test(String(sessionId || ''))) {
|
||||
if (/^code_ai_/.test(String(sessionId || ''))) {
|
||||
if (codeAiLocal) codeAiLocalSessions.add(String(sessionId));
|
||||
else codeAiLocalSessions.delete(String(sessionId));
|
||||
}
|
||||
@@ -11370,7 +11370,7 @@ app.post('/api/chat', async (req, res) => {
|
||||
const isCodeEditorSession = /^(code_ai_|proj-plan-|proj-gen-)/.test(String(sessionId));
|
||||
const histOverride = (isCodeEditorSession && Array.isArray(clientHistory)) ? clientHistory : undefined;
|
||||
// Code editor chat runs on its own clean tool loop — no multi-agent orchestration.
|
||||
const isCodeAiChat = /^(code_ai_|proj-gen-|proj-plan-)/.test(String(sessionId));
|
||||
const isCodeAiChat = /^code_ai_/.test(String(sessionId));
|
||||
// Client sends useTools:false for plan/ask phase — honour it so the model doesn't
|
||||
// loop on list_files when it should just generate a text response.
|
||||
const codeAiUseTools = useTools !== false;
|
||||
|
||||
+12
-36
@@ -210,6 +210,7 @@
|
||||
<span id="csb-folder-label" style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em">폴더 없음</span>
|
||||
<div style="display:flex;gap:4px">
|
||||
<button id="code-local-btn" onclick="openLocalFolder()" title="로컬 폴더 열기" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:14px;line-height:1;padding:0 3px">🖥️</button>
|
||||
<button onclick="codeSaveFile()" title="파일 저장 (Ctrl+S)" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:13px;line-height:1;padding:0 3px">💾</button>
|
||||
<button onclick="codeNewFile()" title="새 파일" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:16px;line-height:1;padding:0 3px">+</button>
|
||||
<button onclick="csbNewFolderRoot()" title="새 폴더" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:13px;line-height:1;padding:0 3px">📁</button>
|
||||
<button onclick="codeSbRefresh()" title="새로고침" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:13px;line-height:1;padding:0 3px">↺</button>
|
||||
@@ -229,18 +230,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 하단: 모델 설정 패널 -->
|
||||
<div style="flex-shrink:0;height:46%;min-height:160px;display:flex;flex-direction:column;overflow:hidden">
|
||||
<div style="flex-shrink:0;display:flex;flex-direction:column;overflow:hidden;border-top:1px solid var(--line)">
|
||||
<!-- 모델 선택 -->
|
||||
<div style="display:flex;flex-direction:column;flex:1;overflow-y:auto;padding:10px 12px;gap:8px">
|
||||
<div class="csb-model-card">
|
||||
<div class="csb-model-label">
|
||||
<span class="csb-model-dot"></span>
|
||||
🤖 Coder AI 모델
|
||||
</div>
|
||||
<div class="csb-model-select-wrap">
|
||||
<select id="csb-model-select" onchange="csbModelChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="csb-session-project-row" class="csb-project-card" style="display:none">
|
||||
<div class="csb-project-label">📁 프로젝트</div>
|
||||
<div class="csb-project-select-wrap">
|
||||
@@ -284,15 +276,6 @@
|
||||
</div>
|
||||
<div style="font-size:10px;color:var(--muted);margin-top:4px">새 터미널 탭 열면 이 경로로 자동 이동</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">코드 저장 디렉토리</div>
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<input type="text" id="csb-codedir" value="code" onchange="csbCodeDirChanged(this.value)"
|
||||
style="flex:1;font-size:12px;padding:6px 8px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);border-radius:8px;font-family:'IBM Plex Mono',monospace">
|
||||
<button onclick="csbCodeDirSave()" title="저장" style="background:var(--brand);color:#fff;border:none;border-radius:8px;padding:6px 10px;font-size:11px;font-weight:700;cursor:pointer;white-space:nowrap">저장</button>
|
||||
</div>
|
||||
<div style="font-size:10px;color:var(--muted);margin-top:4px">워크스페이스 하위 폴더 (기본: code)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">폰트 크기</div>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
@@ -338,14 +321,15 @@
|
||||
<div id="code-view" class="active">
|
||||
<!-- File tabs bar -->
|
||||
<div class="code-tabs-bar">
|
||||
<div id="code-file-tabs" style="display:flex;align-items:stretch"></div>
|
||||
<button class="code-new-tab-btn" onclick="codeNewFile()" title="새 파일">+</button>
|
||||
<div class="code-toolbar">
|
||||
<button id="code-folder-btn" class="code-tool-btn btn-folder" onclick="toggleCodeFolderBrowser(event)" title="파일 탐색기 열기"
|
||||
style="gap:4px;display:flex;align-items:center">
|
||||
📁 <span>workspace/code/</span> ▾
|
||||
</button>
|
||||
<button class="code-tool-btn btn-save" onclick="codeSaveFile()" title="파일 저장 (Ctrl+S)">💾 저장</button>
|
||||
<div id="code-file-tabs" style="display:flex;align-items:stretch;flex:1"></div>
|
||||
<div id="code-ai-model-card" style="display:flex;align-items:center;gap:7px;padding:3px 10px 3px 12px;border-left:1px solid #1a4a35;flex-shrink:0;align-self:stretch">
|
||||
<span style="font-size:16px;line-height:1">🤖</span>
|
||||
<div style="display:flex;flex-direction:column;gap:1px">
|
||||
<span style="font-size:9px;font-weight:700;color:#22c55e;text-transform:uppercase;letter-spacing:.07em;line-height:1">Coder AI</span>
|
||||
<div class="csb-model-select-wrap" style="min-width:120px">
|
||||
<select id="csb-model-select" onchange="csbModelChanged()" style="font-size:12px;font-weight:700"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Diff review bar (full-width, above editor) -->
|
||||
@@ -360,15 +344,6 @@
|
||||
<div id="code-editor-container"></div>
|
||||
<div class="code-vresize" id="code-vresize"></div>
|
||||
<div id="code-ai-panel">
|
||||
<div class="code-ai-header">
|
||||
<span id="code-ai-title">💻 Coder AI</span>
|
||||
<div class="code-ai-tech-selector" id="code-ai-tech-selector">
|
||||
<button class="code-ai-tech-btn active" data-tech="auto" onclick="setCodeAiTech('auto')">🤖 자동</button>
|
||||
<button class="code-ai-tech-btn" data-tech="html" onclick="setCodeAiTech('html')">🌐 HTML</button>
|
||||
<button class="code-ai-tech-btn" data-tech="python" onclick="setCodeAiTech('python')">🐍 Python</button>
|
||||
<button class="code-ai-tech-btn" data-tech="node" onclick="setCodeAiTech('node')">🟢 Node</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="code-ai-messages">
|
||||
<div class="code-ai-msg thinking">코드를 선택하고 "AI에게 전송"을 누르거나, 아래에 질문을 입력하세요.</div>
|
||||
</div>
|
||||
@@ -407,6 +382,7 @@
|
||||
<div id="code-term-tabs" style="display:flex;align-items:stretch;height:100%;flex:1"></div>
|
||||
<button onclick="addLocalShell()" id="local-shell-btn" title="로컬(companion) 탭 추가" style="background:none;border:none;color:#22c55e;cursor:pointer;font-size:16px;padding:0 8px;line-height:1;flex-shrink:0">+</button>
|
||||
<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 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>
|
||||
|
||||
+114
-80
@@ -105,8 +105,6 @@ function codeDetectLang(name) {
|
||||
|
||||
function updateCodeAiLabel() {
|
||||
const label = codeAiModel || 'AI';
|
||||
const title = document.getElementById('code-ai-title');
|
||||
if (title) title.textContent = '💻 Coder-' + label;
|
||||
const input = document.getElementById('code-ai-input');
|
||||
if (input) input.placeholder = label + '에게 질문... (예: 게임 만들어줘)';
|
||||
}
|
||||
@@ -114,15 +112,11 @@ function updateCodeAiLabel() {
|
||||
function setCodeAiTech(tech) {
|
||||
codeAiTech = tech;
|
||||
localStorage.setItem('codeAiTech', tech);
|
||||
document.querySelectorAll('.code-ai-tech-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.tech === tech);
|
||||
});
|
||||
updateCodeAiLabel();
|
||||
}
|
||||
|
||||
function initCodeView() {
|
||||
updateCodeAiLabel();
|
||||
setCodeAiTech(codeAiTech); // Initialize tech selector UI
|
||||
setCodeAiTech(codeAiTech);
|
||||
if (monacoReady) { setTimeout(() => { if (monacoEditor) monacoEditor.layout(); }, 50); return; }
|
||||
if (typeof require === 'undefined') return;
|
||||
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
|
||||
@@ -489,18 +483,8 @@ function _termDetectAndShowError(shellId) {
|
||||
}
|
||||
|
||||
function _showTerminalErrorButton(shellId, errorText) {
|
||||
// Remove old button if present
|
||||
const old = document.getElementById('code-term-ai-error-btn');
|
||||
if (old) old.remove();
|
||||
const footer = document.querySelector('#code-terminal-panel .term-footer, #code-terminal-panel');
|
||||
if (!footer) return;
|
||||
const btn = document.createElement('button');
|
||||
btn.id = 'code-term-ai-error-btn';
|
||||
btn.textContent = '🤖 에러 AI에게 전송';
|
||||
btn.style.cssText = 'position:absolute;bottom:4px;right:90px;z-index:10;background:#dc2626;color:#fff;border:none;border-radius:6px;padding:4px 10px;font-size:11px;cursor:pointer;font-weight:600;animation:pulse-red 1.5s infinite';
|
||||
btn.onclick = () => { _sendTerminalErrorToAI(errorText); btn.remove(); };
|
||||
footer.style.position = 'relative';
|
||||
footer.appendChild(btn);
|
||||
const btn = document.getElementById('code-term-ai-error-btn');
|
||||
if (btn) { btn.style.background = '#dc2626'; btn.style.color = '#fff'; btn.dataset.shellId = shellId; }
|
||||
}
|
||||
|
||||
function _sendTerminalErrorToAI(errorText) {
|
||||
@@ -514,12 +498,21 @@ function _sendTerminalErrorToAI(errorText) {
|
||||
codeAiSend();
|
||||
}
|
||||
|
||||
function _sendActiveTermErrorToAI() {
|
||||
const btn = document.getElementById('code-term-ai-error-btn');
|
||||
const shellId = btn?.dataset.shellId || _lastErrorShellId;
|
||||
const buf = (shellId && _termBuffers[shellId]) || Object.values(_termBuffers).at(-1);
|
||||
const text = buf ? buf.slice(-40).join('\n') : _lastErrorText;
|
||||
if (text) _sendTerminalErrorToAI(text);
|
||||
if (btn) { btn.style.background = '#1a3a28'; btn.style.color = '#22c55e'; }
|
||||
}
|
||||
|
||||
// Called when a run completes successfully (exit code 0) — clear error button
|
||||
function _termOnRunSuccess(shellId) {
|
||||
_lastErrorShellId = null;
|
||||
_lastErrorText = '';
|
||||
const btn = document.getElementById('code-term-ai-error-btn');
|
||||
if (btn) btn.remove();
|
||||
if (btn) btn.style.display = 'none';
|
||||
}
|
||||
|
||||
// ── Companion WebSocket (local Python execution) ───────────────────────────
|
||||
@@ -614,8 +607,6 @@ function _companionCdProject(shellId) {
|
||||
}, 800);
|
||||
}
|
||||
|
||||
function csbCodeDirChanged(val) {}
|
||||
function csbCodeDirSave() {}
|
||||
function csbLoadCodeDir() {}
|
||||
|
||||
async function codeSaveFile() {
|
||||
@@ -2219,9 +2210,23 @@ async function codeAiSend() {
|
||||
if (codeAiAbort) codeAiAbort.abort();
|
||||
return;
|
||||
}
|
||||
// Don't consume the click if there's nothing to send.
|
||||
const inp = document.getElementById('code-ai-input');
|
||||
if (!inp || !inp.value.trim()) return;
|
||||
// 승인 대기 중인 계획 결정 카드가 있으면 입력을 가로채서 버튼 클릭으로 처리
|
||||
const pendingCard = document.querySelector('.code-ai-msg.system button:not([disabled])');
|
||||
if (pendingCard) {
|
||||
const txt = inp.value.trim().toLowerCase();
|
||||
const isYes = /^(응|예|ㅇ|ok|yes|실행|승인|go|ㄱ|ㅇㅇ)$/.test(txt);
|
||||
const isNo = /^(아니|취소|no|ㄴ|cancel)$/.test(txt);
|
||||
const numMatch = txt.match(/^(\d+)$/);
|
||||
if (isNo) { inp.value = ''; document.querySelector('.code-ai-msg.system button[style*="#dc3545"]:not([disabled])')?.click(); return; }
|
||||
if (numMatch) {
|
||||
const optBtn = document.querySelector(`.code-ai-msg.system button[data-opt-num="${numMatch[1]}"]:not([disabled])`);
|
||||
if (optBtn) { inp.value = ''; optBtn.click(); return; }
|
||||
}
|
||||
const confirmBtn = document.querySelector('.code-ai-msg.system button[style*="#28a745"]:not([disabled])');
|
||||
if (isYes && confirmBtn) { inp.value = ''; confirmBtn.click(); return; }
|
||||
}
|
||||
_codeAiSending = true;
|
||||
_setSendBtnBusy(true);
|
||||
try {
|
||||
@@ -2458,7 +2463,7 @@ async function _codeAiSendImpl() {
|
||||
}
|
||||
|
||||
// ── Code 모드: Phase 1 (계획) → confirm → Phase 2 (실행) ──────────────────
|
||||
const planInstruction = '\n\n[편집 계획] 지금은 계획 단계 — 도구를 실행하지 말고 무엇을 할지만 설명해. 코드를 수정/생성/이동해야 한다면: 어떤 파일을 어떻게 바꿀지 한 줄씩 나열해 (예: "move_file: game.js → js/game.js", "edit_file: index.html — script 경로 수정"). 질문이거나 설명만 필요하면: 바로 답해. 승인되면 실행 단계에서 도구로 직접 처리함.';
|
||||
const planInstruction = '\n\n[계획 단계] 실행하지 말고 무엇을 어떻게 바꿀지만 설명해. 질문이면 바로 답해.';
|
||||
|
||||
const planThinkEl = codeAiAppend('thinking', '계획 중...');
|
||||
let planText = '';
|
||||
@@ -2534,11 +2539,15 @@ async function _codeAiSendImpl() {
|
||||
for (const line of planText.split('\n')) {
|
||||
const m = line.match(/^(\d+)[.)]\s+(.+)/);
|
||||
if (!m) continue;
|
||||
if (parseInt(m[1]) !== opts.length + 1) break; // must be sequential
|
||||
if (parseInt(m[1]) !== opts.length + 1) break;
|
||||
opts.push({ num: parseInt(m[1]), label: m[2].replace(/\*\*/g, '').trim() });
|
||||
if (opts.length === 5) break;
|
||||
if (opts.length === 6) break;
|
||||
}
|
||||
return opts.length >= 2 ? opts : [];
|
||||
if (opts.length < 2) return [];
|
||||
// 파일 작업 키워드가 포함된 항목이 있으면 실행 단계 목록 → 선택지 아님
|
||||
const fileOpRe = /edit_file|create_file|move_file|delete_file|replace_lines|insert_after|\.[a-zA-Z]{2,4}(?:\s|$|—|:)/;
|
||||
if (opts.some(o => fileOpRe.test(o.label))) return [];
|
||||
return opts;
|
||||
})();
|
||||
|
||||
// Show confirm / cancel / modify buttons and wait for user decision
|
||||
@@ -2570,63 +2579,69 @@ async function _codeAiSendImpl() {
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
// ── Option chips (shown when model presented numbered alternatives) ──────
|
||||
let modInput = null;
|
||||
if (_parsedOptions.length) {
|
||||
const optLabel = document.createElement('div');
|
||||
optLabel.style.cssText = 'font-size:11px;color:var(--muted);margin-bottom:4px';
|
||||
optLabel.textContent = '어떤 방법으로 진행할까요?';
|
||||
card.appendChild(optLabel);
|
||||
|
||||
if (_parsedOptions.length) {
|
||||
// ── 선택지 모드 ──────────────────────────────────────────────────────────
|
||||
const optLabel = document.createElement('div');
|
||||
optLabel.style.cssText = 'font-size:12px;color:var(--muted);margin-bottom:6px';
|
||||
optLabel.textContent = '방법을 선택하세요:';
|
||||
card.appendChild(optLabel);
|
||||
const optRow = document.createElement('div');
|
||||
optRow.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px';
|
||||
for (const opt of _parsedOptions) {
|
||||
const short = opt.label.length > 28 ? opt.label.slice(0, 28) + '…' : opt.label;
|
||||
const ob = mkBtn(`${opt.num}. ${short}`, '#0d6efd', 'text-align:left;white-space:normal;max-width:220px;line-height:1.3;padding:5px 10px');
|
||||
const short = opt.label.length > 32 ? opt.label.slice(0, 32) + '…' : opt.label;
|
||||
const ob = mkBtn(`${opt.num}. ${short}`, '#0d6efd', 'text-align:left;white-space:normal;max-width:200px;line-height:1.4;padding:5px 10px');
|
||||
ob.title = opt.label;
|
||||
ob.dataset.optNum = String(opt.num);
|
||||
ob.onclick = () => done({ option: opt.num, label: opt.label });
|
||||
allBtns.push(ob);
|
||||
optRow.appendChild(ob);
|
||||
}
|
||||
card.appendChild(optRow);
|
||||
}
|
||||
const cancelBtn = mkBtn('❌ 취소', '#dc3545');
|
||||
allBtns.push(cancelBtn);
|
||||
cancelBtn.onclick = () => done(false);
|
||||
card.appendChild(cancelBtn);
|
||||
} else {
|
||||
// ── 일반 모드: 실행 / 수정 / 취소 ──────────────────────────────────────
|
||||
const hint = document.createElement('div');
|
||||
hint.style.cssText = 'font-size:12px;color:var(--muted);margin-bottom:6px';
|
||||
hint.textContent = '위 계획대로 실행할까요?';
|
||||
card.appendChild(hint);
|
||||
|
||||
// ── Main action row ──────────────────────────────────────────────────────
|
||||
const btnRow = document.createElement('div');
|
||||
btnRow.style.cssText = 'display:flex;gap:8px;align-items:center;flex-wrap:wrap';
|
||||
const hint = _parsedOptions.length
|
||||
? '<span style="font-size:12px;color:var(--muted)">또는:</span>'
|
||||
: '<span style="font-size:12px;color:var(--muted)">위 계획대로 편집을 실행할까요?</span>';
|
||||
btnRow.innerHTML = hint;
|
||||
const btnRow = document.createElement('div');
|
||||
btnRow.style.cssText = 'display:flex;gap:8px;align-items:center;flex-wrap:wrap';
|
||||
|
||||
const confirmBtn = mkBtn('✅ 실행', '#28a745');
|
||||
const modifyBtn = mkBtn('✏️ 수정', '#6c757d');
|
||||
const cancelBtn = mkBtn('❌ 취소', '#dc3545');
|
||||
allBtns.push(confirmBtn, modifyBtn, cancelBtn);
|
||||
const confirmBtn = mkBtn('✅ 실행', '#28a745');
|
||||
const modifyBtn = mkBtn('✏️ 수정', '#6c757d');
|
||||
const cancelBtn = mkBtn('❌ 취소', '#dc3545');
|
||||
allBtns.push(confirmBtn, modifyBtn, cancelBtn);
|
||||
|
||||
// Modify row (hidden until ✏️ clicked)
|
||||
const modRow = document.createElement('div');
|
||||
modRow.style.cssText = 'display:none;margin-top:6px;gap:6px;align-items:flex-start;flex-direction:column';
|
||||
modInput = document.createElement('textarea');
|
||||
modInput.placeholder = '수정 요청 입력 (예: 버튼 색상은 파란색으로)';
|
||||
modInput.style.cssText = 'width:100%;box-sizing:border-box;padding:5px 8px;font-size:12px;border:1px solid var(--border,#444);border-radius:4px;background:var(--panel-2,#1e1e2e);color:var(--text,#ccc);resize:vertical;min-height:54px';
|
||||
const replanBtn = mkBtn('🔄 다시 계획', '#0d6efd');
|
||||
allBtns.push(replanBtn);
|
||||
modRow.appendChild(modInput);
|
||||
modRow.appendChild(replanBtn);
|
||||
// Modify row (hidden until ✏️ clicked)
|
||||
const modRow = document.createElement('div');
|
||||
modRow.style.cssText = 'display:none;margin-top:6px;gap:6px;align-items:flex-start;flex-direction:column';
|
||||
modInput = document.createElement('textarea');
|
||||
modInput.placeholder = '수정 요청 입력 (예: 버튼 색상은 파란색으로)';
|
||||
modInput.style.cssText = 'width:100%;box-sizing:border-box;padding:5px 8px;font-size:12px;border:1px solid var(--border,#444);border-radius:4px;background:var(--panel-2,#1e1e2e);color:var(--text,#ccc);resize:vertical;min-height:54px';
|
||||
const replanBtn = mkBtn('🔄 다시 계획', '#0d6efd');
|
||||
allBtns.push(replanBtn);
|
||||
modRow.appendChild(modInput);
|
||||
modRow.appendChild(replanBtn);
|
||||
|
||||
confirmBtn.onclick = () => done(true);
|
||||
cancelBtn.onclick = () => done(false);
|
||||
modifyBtn.onclick = () => { modRow.style.display = 'flex'; modifyBtn.style.display = 'none'; modInput.focus(); };
|
||||
replanBtn.onclick = () => { const t = modInput.value.trim(); done(t ? { modify: t } : false); };
|
||||
modInput.addEventListener('keydown', e => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) replanBtn.click(); });
|
||||
confirmBtn.onclick = () => done(true);
|
||||
cancelBtn.onclick = () => done(false);
|
||||
modifyBtn.onclick = () => { modRow.style.display = 'flex'; modifyBtn.style.display = 'none'; modInput.focus(); };
|
||||
replanBtn.onclick = () => { const t = modInput.value.trim(); done(t ? { modify: t } : false); };
|
||||
modInput.addEventListener('keydown', e => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) replanBtn.click(); });
|
||||
|
||||
btnRow.appendChild(confirmBtn);
|
||||
btnRow.appendChild(modifyBtn);
|
||||
btnRow.appendChild(cancelBtn);
|
||||
card.appendChild(btnRow);
|
||||
card.appendChild(modRow);
|
||||
} // end else (normal mode)
|
||||
if (signal) signal.addEventListener('abort', onAbort, { once: true });
|
||||
btnRow.appendChild(confirmBtn);
|
||||
btnRow.appendChild(modifyBtn);
|
||||
btnRow.appendChild(cancelBtn);
|
||||
card.appendChild(btnRow);
|
||||
card.appendChild(modRow);
|
||||
msgs.appendChild(card);
|
||||
codeAiScrollBottom();
|
||||
});
|
||||
@@ -2692,8 +2707,9 @@ async function _codeAiSendImpl() {
|
||||
const reader = res.body.getReader();
|
||||
const dec = new TextDecoder();
|
||||
let buf = '', reply = '', _preambleBuf = '', _firstToolSeen = false;
|
||||
thinkEl.textContent = '';
|
||||
thinkEl.className = 'code-ai-msg assistant';
|
||||
let liveTextEl = null, segReply = '';
|
||||
thinkEl.textContent = '⏳';
|
||||
thinkEl.className = 'code-ai-msg thinking';
|
||||
// Live streaming state (mirrored into the editor via codeLiveStreamUpdate)
|
||||
// Pin the diff target to the file that was active when the user hit send —
|
||||
// that's the file we attached as "[현재 파일: …]" context, so any returned
|
||||
@@ -2717,11 +2733,17 @@ async function _codeAiSendImpl() {
|
||||
if (ev.type === 'usage') { _codeAiOnUsage(ev); continue; }
|
||||
if (ev.type === 'token' && (ev.content || ev.text)) token = ev.content || ev.text;
|
||||
else if (ev.type === 'text_delta' && ev.delta) token = ev.delta;
|
||||
else if (ev.type === 'final' && (ev.text || ev.content)) { if (!reply) reply = ev.text || ev.content; thinkEl.textContent = reply; codeAiScrollBottom(); continue; }
|
||||
else if (ev.type === 'done' && ev.reply) { if (!reply) reply = ev.reply; thinkEl.textContent = reply; codeAiScrollBottom(); continue; }
|
||||
else if (ev.type === 'final' && (ev.text || ev.content)) { if (!reply) reply = ev.text || ev.content; (liveTextEl || thinkEl).textContent = reply; codeAiScrollBottom(); continue; }
|
||||
else if (ev.type === 'done' && ev.reply) { if (!reply) reply = ev.reply; (liveTextEl || thinkEl).textContent = reply; codeAiScrollBottom(); continue; }
|
||||
// Handle tool events for live editor update + action cards
|
||||
else if (ev.type === 'tool_call') {
|
||||
_firstToolSeen = true;
|
||||
if (!_firstToolSeen) { _firstToolSeen = true; thinkEl.style.display = 'none'; }
|
||||
// Close current text bubble before showing tool card
|
||||
if (liveTextEl) {
|
||||
if (!segReply.trim()) liveTextEl.remove();
|
||||
else { liveTextEl.classList.remove('streaming'); codeAiRenderMarkdown(liveTextEl, segReply); }
|
||||
liveTextEl = null; segReply = '';
|
||||
}
|
||||
const toolName = ev.action || ev.name || '';
|
||||
const toolArgs = ev.args || {};
|
||||
liveState.toolCallCount = (liveState.toolCallCount || 0) + 1;
|
||||
@@ -2749,12 +2771,11 @@ async function _codeAiSendImpl() {
|
||||
}
|
||||
}
|
||||
if (token) {
|
||||
if (!_firstToolSeen) {
|
||||
// Buffer pre-tool text — likely plan repetition; suppress from UI
|
||||
_preambleBuf += token;
|
||||
} else {
|
||||
reply += token; thinkEl.textContent = reply; codeAiScrollBottom();
|
||||
try { codeLiveStreamUpdate(reply, liveState); } catch(e) { console.error('[codeLiveStream] error:', e); thinkEl.textContent += '\n⚠️ 에디터 스트리밍 오류: ' + (e.message || e); }
|
||||
reply += token; segReply += token;
|
||||
if (!liveTextEl) { liveTextEl = codeAiAppend('assistant', ''); liveTextEl.classList.add('streaming'); }
|
||||
liveTextEl.textContent = segReply; codeAiScrollBottom();
|
||||
if (_firstToolSeen) {
|
||||
try { codeLiveStreamUpdate(segReply, liveState); } catch(e) { console.error('[codeLiveStream] error:', e); liveTextEl.textContent += '\n⚠️ 에디터 스트리밍 오류: ' + (e.message || e); }
|
||||
}
|
||||
}
|
||||
} catch(e) { console.error('[codeAiStream] parse error:', e.message || e, 'data:', data?.slice(0, 80)); }
|
||||
@@ -3116,9 +3137,18 @@ async function _codeAiSendImpl() {
|
||||
if (activeProjectName) saveProjectSession(activeProjectName);
|
||||
try { updateSessionPanel(); } catch (_) {}
|
||||
thinkEl.classList.remove('streaming');
|
||||
if (!reply) { thinkEl.textContent = '(응답 없음)'; }
|
||||
else codeAiRenderMarkdown(thinkEl, reply);
|
||||
if (_firstToolSeen) {
|
||||
thinkEl.style.display = 'none';
|
||||
if (liveTextEl) { liveTextEl.classList.remove('streaming'); codeAiRenderMarkdown(liveTextEl, segReply); }
|
||||
const doneEl = codeAiAppend('system', '✅ 완료');
|
||||
doneEl.style.cssText = 'font-weight:600;color:#4ade80;';
|
||||
} else {
|
||||
thinkEl.className = 'code-ai-msg assistant';
|
||||
if (!reply) { thinkEl.textContent = '(응답 없음)'; thinkEl.style.display = ''; }
|
||||
else { codeAiRenderMarkdown(thinkEl, reply); thinkEl.style.display = ''; }
|
||||
}
|
||||
} catch(e) {
|
||||
thinkEl.style.display = '';
|
||||
thinkEl.className = 'code-ai-msg thinking';
|
||||
const errMsg = (e.name === 'AbortError' || (e.message || '').includes('aborted'))
|
||||
? '요청이 취소되었습니다. 다시 시도해주세요.'
|
||||
@@ -5467,6 +5497,8 @@ function setupCodeVResize() {
|
||||
const handle = document.getElementById('code-vresize');
|
||||
const aiPanel = document.getElementById('code-ai-panel');
|
||||
if (!handle || !aiPanel) return;
|
||||
const modelCard = document.getElementById('code-ai-model-card');
|
||||
const syncModelCard = w => { if (modelCard) modelCard.style.width = w + 'px'; };
|
||||
let startX = 0, startW = 0;
|
||||
handle.addEventListener('mousedown', function(e) {
|
||||
startX = e.clientX; startW = aiPanel.offsetWidth;
|
||||
@@ -5478,6 +5510,7 @@ function setupCodeVResize() {
|
||||
const dx = startX - e.clientX;
|
||||
const newW = Math.max(220, Math.min(window.innerWidth * 0.6, startW + dx));
|
||||
aiPanel.style.width = newW + 'px';
|
||||
syncModelCard(newW);
|
||||
if (monacoEditor) monacoEditor.layout();
|
||||
}
|
||||
function onVUp() {
|
||||
@@ -5502,6 +5535,7 @@ function setupCodeVResize() {
|
||||
try { updateSessionPanel(); } catch (e) {}
|
||||
try { updateCodeAiModeBtn(); } catch (e) {}
|
||||
try { _fetchModelCtxMax(codeAiModel); } catch (e) {}
|
||||
try { const p = document.getElementById('code-ai-panel'), c = document.getElementById('code-ai-model-card'); if (p && c) c.style.width = p.offsetWidth + 'px'; } catch (e) {}
|
||||
try { codeSbRefresh(); } catch (e) {}
|
||||
try { companionConnect(); } catch (e) {}
|
||||
// 히스토리 없으면 세션 목록 표시
|
||||
|
||||
Reference in New Issue
Block a user