feat: Stabilize Company Suite & Self-Reflection logic, integrate new ADRs and bug records
This commit is contained in:
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user