feat: Stabilize Company Suite & Self-Reflection logic, integrate new ADRs and bug records

This commit is contained in:
2026-05-14 16:05:28 +09:00
parent f521c3f557
commit 618b8d5b34
33 changed files with 2203 additions and 655 deletions
+42
View File
@@ -417,6 +417,14 @@
cursor: pointer;
}
.company-agent-model option { color: var(--text-primary); background: var(--bg); }
.company-agent-role-select {
font-size: 10px; padding: 3px 6px; border-radius: 5px;
background: var(--surface); color: var(--text-primary);
border: 1px solid var(--border); cursor: pointer; max-width: 90px;
}
.company-agent-role-select:disabled { opacity: 0.6; cursor: not-allowed; }
.company-agent-role-select.overridden { border-color: var(--accent); color: var(--accent); }
.company-agent-role-select option { color: var(--text-primary); background: var(--bg); }
.company-agent-edit {
background: transparent; border: 1px solid var(--border);
color: var(--text-dim); font-size: 10px;
@@ -513,6 +521,35 @@
.company-agent-editor .editor-actions button.danger { color: var(--error); }
.company-agent-editor .editor-actions button:hover { border-color: var(--border-bright); }
/* Add-agent inline form. Default closed; toggled via [data-open="true"]. */
.company-agent-add-form { display: none; }
.company-agent-add-form[data-open="true"] { display: block; }
.company-agent-add-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 8px 10px;
}
.company-agent-add-grid .field-label {
display: flex; flex-direction: column; gap: 4px;
font-size: 10px; color: var(--text-dim);
}
.company-agent-add-grid input[type="text"],
.company-agent-add-grid textarea {
font-size: 11px; padding: 6px 8px; border-radius: 6px;
background: var(--bg); color: var(--text-primary);
border: 1px solid var(--border); font-family: inherit;
}
.company-agent-add-grid textarea { resize: vertical; min-height: 44px; }
.company-agent-add-form .editor-actions {
display: flex; gap: 6px; justify-content: flex-end;
}
.company-agent-add-form .editor-actions button {
font-size: 10px; padding: 4px 10px; border-radius: 5px; cursor: pointer;
background: var(--surface); color: var(--text-primary);
border: 1px solid var(--border);
}
.company-agent-add-form .editor-actions button.primary {
background: var(--accent); border-color: var(--accent); color: #fff;
}
/* Per-phase company turn header in chat. */
.company-phase-card {
border: 1px solid var(--border);
@@ -684,6 +721,11 @@
.history-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8);
backdrop-filter: blur(10px); z-index: 1000; display: none; flex-direction: column; padding: 20px;
/* 본문이 뷰포트보다 길어지면 자체 스크롤. companyOverlay의 에이전트 목록처럼
동적으로 늘어나는 컨텐츠가 잘리는 문제를 막는다.
`historyOverlay`처럼 내부에 별도 스크롤 div를 둔 곳도 있는데
그쪽은 본인 wrapper가 우선이라 영향 없음. */
overflow-y: auto;
}
.history-overlay.visible { display: flex; }
+94
View File
@@ -165,10 +165,104 @@
<div class="map-section-title">활성 에이전트 + 모델</div>
<div class="map-section-hint">CEO는 항상 활성. 각 에이전트별로 모델을 따로 지정할 수 있습니다 — 다른 모델을 쓸 때만 LM Studio가 swap합니다.</div>
</div>
<div class="map-btn-group">
<button class="secondary-btn" id="addCompanyAgentBtn" title="새 사용자 에이전트 추가">+ Agent</button>
</div>
</div>
<ul id="companyAgentList" class="map-list company-agent-list"></ul>
</div>
<!-- Inline form for adding a new custom agent. Hidden by default; the
"+ Agent" button toggles `data-open` to show it. Kept in-overlay
(no separate modal) so the user can see existing agents while
editing — easier to spot id collisions. -->
<div id="addCompanyAgentForm" class="map-section company-agent-add-form" data-open="false">
<div class="map-section-title" style="margin-bottom:8px;">새 에이전트 추가</div>
<div class="company-agent-add-grid">
<label class="field-label">ID (소문자/숫자/-/_, 예: marketer)
<input type="text" id="newAgentId" placeholder="marketer" />
</label>
<label class="field-label">이름
<input type="text" id="newAgentName" placeholder="현수" />
</label>
<label class="field-label">역할
<input type="text" id="newAgentRole" placeholder="Marketing Lead" />
</label>
<label class="field-label">이모지
<input type="text" id="newAgentEmoji" placeholder="📣" maxlength="4" />
</label>
<label class="field-label">색상 (#hex)
<input type="text" id="newAgentColor" placeholder="#3B82F6" />
</label>
<label class="field-label" style="grid-column:1/-1;">직군 (담당 역할 분류)
<select id="newAgentRoleCategory"></select>
</label>
<label class="field-label" style="grid-column:1/-1;">한 줄 태그라인
<input type="text" id="newAgentTagline" placeholder="브랜드 메시지·캠페인을 설계합니다" />
</label>
<label class="field-label" style="grid-column:1/-1;">전문 분야 (CEO가 매칭에 사용)
<textarea id="newAgentSpecialty" rows="2" placeholder="캠페인 기획, 메시지 설계, 채널 분석"></textarea>
</label>
<label class="field-label" style="grid-column:1/-1;">페르소나 (선택)
<textarea id="newAgentPersona" rows="2" placeholder="데이터 기반·간결한 톤. 가설 검증 사이클을 좋아함."></textarea>
</label>
</div>
<div class="editor-actions" style="margin-top:10px;">
<button id="cancelAddAgentBtn">Cancel</button>
<button class="primary" id="saveAddAgentBtn">추가</button>
</div>
<div id="addAgentError" class="map-status" style="color:var(--error); margin-top:6px;"></div>
</div>
<!-- Work Pipeline editor. The active pipeline (if any) drives the
dispatcher instead of the CEO planner. Empty list / "기본 (CEO
자유 분배)" → legacy planner behaviour. -->
<div class="map-section">
<div class="map-section-head">
<div>
<div class="map-section-title">워크 파이프라인</div>
<div class="map-section-hint">CEO 자유 분배 대신 사용자가 정한 stage 순서대로 dispatch합니다. loop-back 정규식이 매칭되면 이전 stage로 되돌아갑니다 (최대 maxIterations 회).</div>
</div>
<div class="map-btn-group">
<button class="secondary-btn" id="addCompanyPipelineBtn" title="새 파이프라인 추가">+ Pipeline</button>
</div>
</div>
<div class="control-row" style="margin-top:8px; gap:8px; align-items:center;">
<label style="font-size:10px; color:var(--text-dim);">활성:</label>
<select id="activePipelineSel" style="flex:1; padding:4px 8px; background:var(--bg); color:var(--text-primary); border:1px solid var(--border); border-radius:6px; font-size:11px;">
<option value="">기본 (CEO 자유 분배)</option>
</select>
</div>
<ul id="companyPipelineList" class="map-list" style="margin-top:8px;"></ul>
</div>
<!-- Pipeline editor: hidden until "+ Pipeline" or "Edit" is clicked.
MVP is a JSON textarea so we don't have to ship a stage-list
reorder widget yet. -->
<div id="pipelineEditForm" class="map-section company-agent-add-form" data-open="false">
<div class="map-section-title" style="margin-bottom:8px;">파이프라인 편집</div>
<div class="company-agent-add-grid">
<label class="field-label">ID
<input type="text" id="pipelineEditId" placeholder="product-dev-v1" />
</label>
<label class="field-label">이름
<input type="text" id="pipelineEditName" placeholder="제품 개발 v1" />
</label>
<label class="field-label" style="grid-column:1/-1;">Stages (JSON 배열)
<textarea id="pipelineEditStages" rows="14" style="font-family:monospace; font-size:11px;"></textarea>
</label>
</div>
<div class="map-section-hint" style="margin-top:6px;">
예시:
<code style="font-size:10px;">[{"id":"plan","label":"기획","agentId":"writer","instructionTemplate":"{{userPrompt}} 에 대한 기획서 작성"},{"id":"dev","label":"개발","agentId":"developer","instructionTemplate":"다음 기획대로 구현: {{stage.plan}}","loopBackPattern":"버그|오류|fail","loopBackTo":"plan","maxIterations":3}]</code>
</div>
<div class="editor-actions" style="margin-top:10px;">
<button id="cancelPipelineEditBtn">Cancel</button>
<button class="primary" id="savePipelineEditBtn">저장</button>
</div>
<div id="pipelineEditError" class="map-status" style="color:var(--error); margin-top:6px;"></div>
</div>
<div class="map-footer">
<button class="secondary-btn" id="openCompanySessionsBtn" title="이번 회사가 만든 세션 폴더 열기">세션 폴더 열기</button>
<div style="flex:1"></div>
+335
View File
@@ -633,6 +633,9 @@
// 생성 완료 시 Stop 버튼 숨기고 Send 복구
setGenerating(false);
resetStepper();
// 1인 기업 모드는 streamStart를 거치지 않아 thinkingBar가
// 그대로 남으므로 streamEnd에서 명시적으로 끄는 게 안전.
thinkingBar.classList.remove('active');
Sound.success();
vscode.postMessage({ type: 'getReadyStatus' });
break;
@@ -793,6 +796,64 @@
renderCompanyAgentCards(msg.value || {});
break;
}
case 'addCompanyAgentResult': {
const v = msg.value || {};
const errEl = document.getElementById('addAgentError');
const form = document.getElementById('addCompanyAgentForm');
if (v.ok) {
if (errEl) errEl.textContent = '';
// 폼 닫고 필드 초기화 — companyAgents 메시지가 목록을 다시 그려줌
if (form) form.setAttribute('data-open', 'false');
const fids = ['newAgentId','newAgentName','newAgentRole','newAgentEmoji','newAgentColor','newAgentTagline','newAgentSpecialty','newAgentPersona'];
for (const fid of fids) { const el = document.getElementById(fid); if (el) el.value = ''; }
showToast(`✅ '${v.agentId}' 에이전트 추가 완료`, 'info');
} else {
if (errEl) errEl.textContent = v.reason || '에이전트 추가 실패.';
}
break;
}
case 'deleteCompanyAgentResult': {
const v = msg.value || {};
if (v.ok) {
showToast(`🗑 '${v.agentId}' 에이전트 삭제됨`, 'warn');
} else {
showToast(`삭제 실패: ${v.reason || '알 수 없음'}`, 'warn');
}
break;
}
case 'companyPipelines': {
if (typeof window.__renderCompanyPipelines === 'function') {
window.__renderCompanyPipelines(msg.value || {});
}
break;
}
case 'upsertCompanyPipelineResult': {
const v = msg.value || {};
const errEl = document.getElementById('pipelineEditError');
if (v.ok) {
if (errEl) errEl.textContent = '';
if (typeof window.__closePipelineEditor === 'function') window.__closePipelineEditor();
showToast('✅ 파이프라인 저장 완료', 'info');
} else {
if (errEl) errEl.textContent = v.reason || '파이프라인 저장 실패.';
}
break;
}
case 'deleteCompanyPipelineResult': {
const v = msg.value || {};
if (v.ok) showToast(`🗑 파이프라인 '${v.pipelineId}' 삭제됨`, 'warn');
else showToast(`삭제 실패: ${v.reason || '알 수 없음'}`, 'warn');
break;
}
case 'setActiveCompanyPipelineResult': {
const v = msg.value || {};
if (v.ok) {
showToast(v.pipelineId ? `▶ 파이프라인 '${v.pipelineId}' 활성` : '▶ 기본(CEO 자유 분배)로 복귀', 'info');
} else {
showToast(`활성 설정 실패: ${v.reason || '알 수 없음'}`, 'warn');
}
break;
}
case 'openCompanyManageOverlay': {
// Triggered by the Command Palette `Manage 1인 기업 Agents`.
document.getElementById('companyOverlay')?.classList.add('visible');
@@ -1475,6 +1536,7 @@
_companyOverlay.classList.add('visible');
_companyStatusEl.textContent = '불러오는 중...';
vscode.postMessage({ type: 'getCompanyAgents' });
vscode.postMessage({ type: 'getCompanyPipelines' });
};
}
for (const btn of _closeCompanyBtns) {
@@ -1486,6 +1548,218 @@
};
}
// ── Add-agent form: toggle, clear, submit ──
const _addCompanyAgentBtn = document.getElementById('addCompanyAgentBtn');
const _addAgentForm = document.getElementById('addCompanyAgentForm');
const _cancelAddAgentBtn = document.getElementById('cancelAddAgentBtn');
const _saveAddAgentBtn = document.getElementById('saveAddAgentBtn');
const _addAgentError = document.getElementById('addAgentError');
const _addAgentFields = () => ({
id: document.getElementById('newAgentId'),
name: document.getElementById('newAgentName'),
role: document.getElementById('newAgentRole'),
emoji: document.getElementById('newAgentEmoji'),
color: document.getElementById('newAgentColor'),
roleCategory: document.getElementById('newAgentRoleCategory'),
tagline: document.getElementById('newAgentTagline'),
specialty: document.getElementById('newAgentSpecialty'),
persona: document.getElementById('newAgentPersona'),
});
const _populateAddAgentRoleSelect = () => {
const sel = document.getElementById('newAgentRoleCategory');
if (!sel) return;
sel.innerHTML = '';
for (const cat of _roleCategoryOrder) {
if (cat === 'ceo') continue; // ceo는 빌트인 전용
const opt = document.createElement('option');
opt.value = cat;
opt.textContent = _roleCategoryLabels[cat] || cat;
sel.appendChild(opt);
}
sel.value = 'support'; // 기본은 가장 무해한 직군
};
const _clearAddAgentForm = () => {
const f = _addAgentFields();
for (const k of Object.keys(f)) {
if (!f[k]) continue;
if (k === 'roleCategory') continue; // select는 별도 populate
f[k].value = '';
}
if (_addAgentError) _addAgentError.textContent = '';
_populateAddAgentRoleSelect();
};
if (_addCompanyAgentBtn && _addAgentForm) {
_addCompanyAgentBtn.onclick = () => {
const open = _addAgentForm.getAttribute('data-open') === 'true';
_addAgentForm.setAttribute('data-open', open ? 'false' : 'true');
if (!open) {
_clearAddAgentForm();
_addAgentFields().id?.focus();
}
};
}
if (_cancelAddAgentBtn && _addAgentForm) {
_cancelAddAgentBtn.onclick = () => {
_addAgentForm.setAttribute('data-open', 'false');
_clearAddAgentForm();
};
}
if (_saveAddAgentBtn) {
_saveAddAgentBtn.onclick = () => {
const f = _addAgentFields();
const def = {
id: (f.id?.value || '').trim().toLowerCase(),
name: (f.name?.value || '').trim(),
role: (f.role?.value || '').trim(),
emoji: (f.emoji?.value || '').trim(),
color: (f.color?.value || '').trim(),
roleCategory: (f.roleCategory?.value || 'support'),
tagline: (f.tagline?.value || '').trim(),
specialty: (f.specialty?.value || '').trim(),
persona: (f.persona?.value || '').trim(),
};
if (!def.id || !def.name || !def.role) {
if (_addAgentError) _addAgentError.textContent = 'id · 이름 · 역할은 필수입니다.';
return;
}
if (_addAgentError) _addAgentError.textContent = '';
vscode.postMessage({ type: 'addCompanyAgent', def });
};
}
// ── Work Pipeline editor ──
const _activePipelineSel = document.getElementById('activePipelineSel');
const _pipelineList = document.getElementById('companyPipelineList');
const _addPipelineBtn = document.getElementById('addCompanyPipelineBtn');
const _pipelineEditForm = document.getElementById('pipelineEditForm');
const _pipelineEditId = document.getElementById('pipelineEditId');
const _pipelineEditName = document.getElementById('pipelineEditName');
const _pipelineEditStages = document.getElementById('pipelineEditStages');
const _pipelineEditError = document.getElementById('pipelineEditError');
const _cancelPipelineEditBtn = document.getElementById('cancelPipelineEditBtn');
const _savePipelineEditBtn = document.getElementById('savePipelineEditBtn');
const _openPipelineEditor = (pipeline) => {
if (!_pipelineEditForm) return;
_pipelineEditForm.setAttribute('data-open', 'true');
if (_pipelineEditError) _pipelineEditError.textContent = '';
if (pipeline) {
if (_pipelineEditId) { _pipelineEditId.value = pipeline.id; _pipelineEditId.disabled = true; }
if (_pipelineEditName) _pipelineEditName.value = pipeline.name || '';
if (_pipelineEditStages) _pipelineEditStages.value = JSON.stringify(pipeline.stages || [], null, 2);
} else {
if (_pipelineEditId) { _pipelineEditId.value = ''; _pipelineEditId.disabled = false; }
if (_pipelineEditName) _pipelineEditName.value = '';
if (_pipelineEditStages) _pipelineEditStages.value = '[]';
}
};
const _closePipelineEditor = () => {
if (_pipelineEditForm) _pipelineEditForm.setAttribute('data-open', 'false');
if (_pipelineEditId) _pipelineEditId.disabled = false;
};
if (_addPipelineBtn) {
_addPipelineBtn.onclick = () => {
_openPipelineEditor(null);
_pipelineEditId?.focus();
};
}
if (_cancelPipelineEditBtn) {
_cancelPipelineEditBtn.onclick = _closePipelineEditor;
}
if (_savePipelineEditBtn) {
_savePipelineEditBtn.onclick = () => {
let stages;
try {
stages = JSON.parse(_pipelineEditStages?.value || '[]');
} catch (e) {
if (_pipelineEditError) _pipelineEditError.textContent = `Stages JSON 파싱 실패: ${e.message}`;
return;
}
if (!Array.isArray(stages)) {
if (_pipelineEditError) _pipelineEditError.textContent = 'Stages는 배열이어야 합니다.';
return;
}
const def = {
id: (_pipelineEditId?.value || '').trim().toLowerCase(),
name: (_pipelineEditName?.value || '').trim(),
stages,
};
if (!def.id) {
if (_pipelineEditError) _pipelineEditError.textContent = 'id는 필수입니다 (소문자/숫자/-/_).';
return;
}
vscode.postMessage({ type: 'upsertCompanyPipeline', def });
};
}
if (_activePipelineSel) {
_activePipelineSel.onchange = () => {
vscode.postMessage({
type: 'setActiveCompanyPipeline',
pipelineId: _activePipelineSel.value || null,
});
};
}
const renderCompanyPipelines = (payload) => {
if (!_pipelineList || !_activePipelineSel) return;
const pipelines = (payload && payload.pipelines && typeof payload.pipelines === 'object') ? payload.pipelines : {};
const activeId = payload && payload.activePipelineId ? payload.activePipelineId : '';
// active dropdown
_activePipelineSel.innerHTML = '<option value="">기본 (CEO 자유 분배)</option>';
for (const p of Object.values(pipelines)) {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = `${p.name || p.id} (${(p.stages || []).length} stages)`;
_activePipelineSel.appendChild(opt);
}
_activePipelineSel.value = activeId;
// list
_pipelineList.innerHTML = '';
const entries = Object.values(pipelines);
if (entries.length === 0) {
const li = document.createElement('li');
li.className = 'map-item';
li.style.cssText = 'color: var(--text-dim); font-style: italic; padding: 6px 4px;';
li.textContent = '아직 파이프라인이 없습니다. "+ Pipeline" 으로 새로 만드세요.';
_pipelineList.appendChild(li);
return;
}
for (const p of entries) {
const li = document.createElement('li');
li.className = 'map-item';
li.style.cssText = 'padding:8px 10px; background:var(--surface); border:1px solid var(--border); border-radius:6px;';
const head = document.createElement('div');
head.style.cssText = 'display:flex; gap:8px; align-items:center; justify-content:space-between;';
const title = document.createElement('div');
title.innerHTML = `<strong>${escAttr(p.name || p.id)}</strong> <span style="font-size:10px; color:var(--text-dim)">${escAttr(p.id)} · ${(p.stages || []).length} stages${p.id === activeId ? ' · <span style="color:var(--accent)">● 활성</span>' : ''}</span>`;
const actions = document.createElement('div');
actions.style.cssText = 'display:flex; gap:4px;';
const editBtn = document.createElement('button');
editBtn.className = 'company-agent-edit';
editBtn.textContent = '✎ 편집';
editBtn.onclick = () => _openPipelineEditor(p);
const delBtn = document.createElement('button');
delBtn.className = 'company-agent-edit';
delBtn.textContent = '🗑';
delBtn.title = '파이프라인 삭제';
delBtn.onclick = () => {
if (!confirm(`'${p.name || p.id}' 파이프라인을 삭제할까요?`)) return;
vscode.postMessage({ type: 'deleteCompanyPipeline', pipelineId: p.id });
};
actions.appendChild(editBtn);
actions.appendChild(delBtn);
head.appendChild(title);
head.appendChild(actions);
li.appendChild(head);
_pipelineList.appendChild(li);
}
};
// expose for the message handler below
window.__renderCompanyPipelines = renderCompanyPipelines;
window.__closePipelineEditor = _closePipelineEditor;
/**
* Keep the last payload around so we can re-render whenever the
* model list refreshes (the top `#modelSel` is the source of truth
@@ -1531,6 +1805,16 @@
* - an Edit button that toggles an inline prompt editor with
* tagline / specialty / persona textareas + Reset/Save/Cancel.
*/
// 직군 라벨/순서 캐시. 페이로드에 같이 오므로 첫 렌더 후엔 항상 채워져 있음.
// pipeline 에디터(다음 phase)도 같은 캐시를 본다.
let _roleCategoryLabels = {
ceo: 'CEO', planner: '기획', researcher: '리서치', designer: '디자인',
developer: '개발', qa: 'QA', inspector: '감리', support: '지원',
};
let _roleCategoryOrder = ['ceo','planner','researcher','designer','developer','qa','inspector','support'];
window.__getRoleLabels = () => _roleCategoryLabels;
window.__getRoleOrder = () => _roleCategoryOrder;
function renderCompanyAgentCards(payload) {
if (!_companyAgentList) return;
_lastCompanyAgentsPayload = payload;
@@ -1538,6 +1822,8 @@
if (_companyNameInput && payload && typeof payload.companyName === 'string') {
_companyNameInput.value = payload.companyName;
}
if (payload && payload.roleCategoryLabels) _roleCategoryLabels = payload.roleCategoryLabels;
if (payload && Array.isArray(payload.roleCategoryOrder)) _roleCategoryOrder = payload.roleCategoryOrder;
const agents = (payload && Array.isArray(payload.agents)) ? payload.agents : [];
for (const a of agents) {
const li = document.createElement('li');
@@ -1571,6 +1857,38 @@
const controls = document.createElement('div');
controls.className = 'company-agent-controls';
// 직군 select — CEO는 잠금. role 라벨 보다 먼저 와서 시선 흐름이
// "이 사람은 [직군] 직군"으로 자연스럽게 읽히도록 controls 첫번째에.
const roleSelEl = document.createElement('select');
roleSelEl.className = 'company-agent-role-select';
roleSelEl.title = '이 에이전트의 직군. 파이프라인 에디터에서 직군별로 담당자를 고를 때 사용됩니다.';
if (a.id === 'ceo') {
const opt = document.createElement('option');
opt.value = 'ceo'; opt.textContent = _roleCategoryLabels.ceo || 'CEO';
roleSelEl.appendChild(opt);
roleSelEl.value = 'ceo';
roleSelEl.disabled = true;
} else {
for (const cat of _roleCategoryOrder) {
if (cat === 'ceo') continue;
const opt = document.createElement('option');
opt.value = cat;
opt.textContent = _roleCategoryLabels[cat] || cat;
roleSelEl.appendChild(opt);
}
roleSelEl.value = a.roleCategory || a.defaultRoleCategory || 'support';
if (a.roleCategoryOverridden) roleSelEl.classList.add('overridden');
roleSelEl.onchange = () => {
const v = roleSelEl.value;
const sendValue = v === a.defaultRoleCategory ? null : v;
vscode.postMessage({
type: 'setCompanyAgentRoleCategory',
agentId: a.id,
value: sendValue,
});
};
}
const modelSelEl = document.createElement('select');
modelSelEl.className = 'company-agent-model';
modelSelEl.title = '비워두면 글로벌 기본 모델 사용';
@@ -1613,8 +1931,22 @@
vscode.postMessage({ type: 'setCompanyActiveAgents', value: nextIds });
};
}
controls.appendChild(roleSelEl);
controls.appendChild(modelSelEl);
controls.appendChild(editBtn);
// 사용자 추가 에이전트만 삭제 가능. 기본 9명은 코드에 박혀 있어
// 백엔드도 거부하므로 UI에서도 버튼을 노출하지 않음.
if (a.custom) {
const delBtn = document.createElement('button');
delBtn.className = 'company-agent-edit';
delBtn.textContent = '🗑';
delBtn.title = `'${a.name}' 에이전트 삭제`;
delBtn.onclick = () => {
if (!confirm(`'${a.name}' 에이전트를 삭제할까요? 이 에이전트의 모든 설정(모델·프롬프트·지식 믹스)도 함께 삭제됩니다.`)) return;
vscode.postMessage({ type: 'deleteCompanyAgent', agentId: a.id });
};
controls.appendChild(delBtn);
}
controls.appendChild(toggle);
row.appendChild(emoji);
@@ -1837,6 +2169,9 @@
const body = (o.response || '').slice(0, 4000);
card.innerHTML = `<div class="cph-head">${escAttr(ev.agentId)} 완료 <span class="cph-meta">${(o.durationMs/1000).toFixed(1)}s${o.error ? ' · ⚠️ ' + escAttr(o.error) : ''}</span></div>
<div class="markdown-body">${fmt(body)}</div>`;
} else if (ev.phase === 'stage-loop') {
card.innerHTML = `<div class="cph-head">🔁 Stage 재시도</div>
<div class="cph-meta">${escAttr(ev.from)}${escAttr(ev.to)} (반복 ${ev.iteration}회)</div>`;
} else if (ev.phase === 'report-start') {
card.innerHTML = '<div class="cph-head">🧭 CEO 종합 보고서 작성 중…</div>';
} else if (ev.phase === 'report-done') {