feat: Stabilize Company Suite & Self-Reflection logic, integrate new ADRs and bug records
This commit is contained in:
@@ -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; }
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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