release: v2.0.3 - AI 1-Person Company Engine & Business Intelligence

This commit is contained in:
g1nation
2026-05-13 23:22:00 +09:00
parent c40571b7ef
commit b6899851c3
31 changed files with 2504 additions and 41 deletions
+89
View File
@@ -321,6 +321,95 @@
.input-footer { display: flex; align-items: center; justify-content: space-between; }
.footer-left { display: flex; align-items: center; gap: 8px; }
/* Company chip — sits in the records-line beside the Records ▾ menu. */
.company-chip {
display: inline-flex; align-items: center; gap: 5px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 999px;
padding: 3px 10px;
color: var(--text-dim);
font-size: 11px; font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
}
.company-chip:hover { border-color: var(--border-bright); color: var(--text-primary); }
.company-chip[data-active="true"] {
background: var(--accent-glow);
border-color: var(--accent);
color: var(--accent);
}
.company-chip-icon { font-size: 12px; }
.company-manage-btn { padding: 2px 6px; font-size: 11px; margin-left: 2px; }
.company-name-input {
flex: 1; background: var(--input-bg); border: 1px solid var(--border);
border-radius: 6px; padding: 6px 10px; color: var(--text-primary); font-size: 12px;
}
.company-name-input:focus { border-color: var(--accent); outline: none; }
/* Agent cards inside the manage overlay. */
.company-agent-list { display: flex; flex-direction: column; gap: 6px; padding: 0; }
.company-agent-card {
display: flex; align-items: center; gap: 10px;
padding: 8px 10px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
list-style: none;
}
.company-agent-card[data-active="false"] { opacity: 0.55; }
.company-agent-card[data-locked="true"] .company-agent-toggle { cursor: not-allowed; opacity: 0.4; }
.company-agent-emoji {
font-size: 18px; flex-shrink: 0;
display: inline-flex; align-items: center; justify-content: center;
width: 28px; height: 28px;
border-radius: 6px; background: var(--bg-secondary);
}
.company-agent-body { flex: 1; min-width: 0; line-height: 1.35; }
.company-agent-name {
color: var(--text-bright); font-weight: 600; font-size: 12px;
display: flex; gap: 6px; align-items: baseline; flex-wrap: wrap;
}
.company-agent-role { color: var(--text-dim); font-size: 10px; }
.company-agent-tagline {
color: var(--text-primary); font-size: 10.5px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
margin-top: 1px;
}
.company-agent-controls {
display: flex; align-items: center; gap: 6px; flex-shrink: 0;
}
.company-agent-toggle {
background: transparent; border: 1px solid var(--border);
color: var(--text-dim); font-size: 10px; font-weight: 600;
padding: 3px 8px; border-radius: 999px; cursor: pointer;
}
.company-agent-card[data-active="true"] .company-agent-toggle {
border-color: var(--accent); color: var(--accent);
}
.company-agent-model {
background: var(--input-bg); border: 1px solid var(--border);
color: var(--text-primary); font-size: 10px;
padding: 3px 6px; border-radius: 6px; max-width: 130px;
}
/* Per-phase company turn header in chat. */
.company-phase-card {
border: 1px solid var(--border);
background: var(--surface);
border-radius: 8px;
padding: 8px 10px;
margin: 4px 0;
font-size: 11px;
color: var(--text-primary);
}
.company-phase-card .cph-head {
color: var(--text-bright); font-weight: 600;
display: flex; gap: 6px; align-items: center; margin-bottom: 4px;
}
.company-phase-card .cph-meta { color: var(--text-dim); font-size: 10px; }
.company-phase-card.report .cph-head { color: var(--accent); }
/* Project Architecture chip — sits just above the input when project mode is on. */
.arch-chip {
display: none;
+58
View File
@@ -106,6 +106,16 @@
<span id="chronicleAutoStatus" title="Project records are saved automatically after meaningful project turns.">Auto Records</span>
<span class="rl-latest" id="recordsLatest"></span>
</div>
<!--
Company-mode chip. Click toggles enabled; the ▾ opens the manage
overlay. The chip stays visible at all times so the user can flip
into 1인 기업 mode from anywhere in the chat surface.
-->
<button class="company-chip" id="companyChip" data-active="false" title="1인 기업 모드 토글">
<span class="company-chip-icon">🏢</span>
<span class="company-chip-label" id="companyChipLabel">Company OFF</span>
</button>
<button class="icon-btn company-manage-btn" id="companyManageBtn" data-tooltip="회사 관리 (에이전트·모델 설정)"></button>
<div class="hdr-dropdown" data-dd>
<button class="icon-btn" id="recordsMenuBtn" data-dd-trigger data-tooltip="Chronicle records">Records ▾</button>
<div class="hdr-menu hdr-menu-wide" id="recordsMenu" data-dd-menu>
@@ -118,6 +128,54 @@
</div>
</div>
<!--
Company manage overlay. Uses the same overlay framework as the agent
knowledge map modal (`.history-overlay` / `.visible`) so styling and
keyboard dismissal stay consistent.
-->
<div id="companyOverlay" class="history-overlay">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:14px;">
<div>
<h2 style="color:var(--text-bright); margin:0;">🏢 1인 기업 모드</h2>
<p style="margin:4px 0 0; font-size:11px; color:var(--text-dim);">
CEO가 사용자의 요청을 분석하고 활성화된 specialist에게 순차 dispatch합니다.
동시에 메모리에 올라가는 모델은 항상 1개입니다.
</p>
</div>
<button class="icon-btn" id="closeCompanyOverlayBtn"></button>
</div>
<div class="map-section">
<div class="map-section-head">
<div>
<div class="map-section-title">회사 정보</div>
<div class="map-section-hint">CEO와 보고서에 사용되는 회사명. 한국어/영어 모두 가능.</div>
</div>
</div>
<div class="control-row" style="margin-top:8px;">
<input id="companyNameInput" type="text" class="company-name-input" placeholder="회사명 (예: My Company)" />
<button class="secondary-btn" id="saveCompanyNameBtn">저장</button>
</div>
</div>
<div class="map-section">
<div class="map-section-head">
<div>
<div class="map-section-title">활성 에이전트 + 모델</div>
<div class="map-section-hint">CEO는 항상 활성. 각 에이전트별로 모델을 따로 지정할 수 있습니다 — 다른 모델을 쓸 때만 LM Studio가 swap합니다.</div>
</div>
</div>
<ul id="companyAgentList" class="map-list company-agent-list"></ul>
</div>
<div class="map-footer">
<button class="secondary-btn" id="openCompanySessionsBtn" title="이번 회사가 만든 세션 폴더 열기">세션 폴더 열기</button>
<div style="flex:1"></div>
<button class="send-btn" id="closeCompanyOverlayBtn2">닫기</button>
</div>
<div id="companyStatus" class="map-status"></div>
</div>
<div id="historyOverlay" class="history-overlay">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h2 style="color:var(--text-bright);">Chat History</h2>
+194
View File
@@ -777,6 +777,25 @@
vscode.postMessage({ type: 'getKnowledgeScope', agentPath: msg.selected });
syncContextBar();
break;
case 'companyStatus': {
const v = msg.value || {};
renderCompanyChip(!!v.enabled, v.summary || '');
break;
}
case 'companyAgents': {
renderCompanyAgentCards(msg.value || {});
break;
}
case 'openCompanyManageOverlay': {
// Triggered by the Command Palette `Manage 1인 기업 Agents`.
document.getElementById('companyOverlay')?.classList.add('visible');
vscode.postMessage({ type: 'getCompanyAgents' });
break;
}
case 'companyTurnUpdate': {
if (msg.value) renderCompanyPhase(msg.value);
break;
}
case 'architectureStatus': {
// Show / hide the chip + reflect current state.
const chip = document.getElementById('archChip');
@@ -1350,6 +1369,7 @@
vscode.postMessage({ type: 'getChronicleRecords' });
vscode.postMessage({ type: 'getKnowledgeMix' });
vscode.postMessage({ type: 'getArchitectureStatus' });
vscode.postMessage({ type: 'getCompanyStatus' });
vscode.postMessage({ type: 'ready' });
// ── Project Architecture chip buttons ─────────────────────────────────
@@ -1360,6 +1380,180 @@
if (_archRefreshBtn) _archRefreshBtn.onclick = () => vscode.postMessage({ type: 'refreshArchitecture' });
if (_archDetachBtn) _archDetachBtn.onclick = () => vscode.postMessage({ type: 'detachArchitecture' });
// ── 1인 기업 (Company) Mode chip + manage overlay ─────────────────────
// The chip itself toggles enabled/disabled. The ▾ button opens the
// manage overlay where the user picks active agents + per-agent
// model overrides. State round-trips through `companyStatus` /
// `companyAgents` messages so the webview and extension stay in sync.
const _companyChip = document.getElementById('companyChip');
const _companyChipLabel = document.getElementById('companyChipLabel');
const _companyManageBtn = document.getElementById('companyManageBtn');
const _companyOverlay = document.getElementById('companyOverlay');
const _closeCompanyBtns = [
document.getElementById('closeCompanyOverlayBtn'),
document.getElementById('closeCompanyOverlayBtn2'),
].filter(Boolean);
const _companyNameInput = document.getElementById('companyNameInput');
const _saveCompanyNameBtn = document.getElementById('saveCompanyNameBtn');
const _companyAgentList = document.getElementById('companyAgentList');
const _companyStatusEl = document.getElementById('companyStatus');
const renderCompanyChip = (active, summary) => {
if (!_companyChip || !_companyChipLabel) return;
_companyChip.setAttribute('data-active', active ? 'true' : 'false');
_companyChipLabel.textContent = active ? (summary || 'Company ON') : 'Company OFF';
};
if (_companyChip) {
_companyChip.onclick = () => {
const isActive = _companyChip.getAttribute('data-active') === 'true';
// Optimistic flip — backend echoes the canonical state back.
renderCompanyChip(!isActive, _companyChipLabel?.textContent || '');
vscode.postMessage({ type: 'setCompanyEnabled', value: !isActive });
};
}
if (_companyManageBtn) {
_companyManageBtn.onclick = () => {
if (!_companyOverlay) return;
_companyOverlay.classList.add('visible');
_companyStatusEl.textContent = '불러오는 중...';
vscode.postMessage({ type: 'getCompanyAgents' });
};
}
for (const btn of _closeCompanyBtns) {
btn.onclick = () => _companyOverlay?.classList.remove('visible');
}
if (_saveCompanyNameBtn && _companyNameInput) {
_saveCompanyNameBtn.onclick = () => {
vscode.postMessage({ type: 'setCompanyName', value: _companyNameInput.value });
};
}
/**
* Render the agent cards in the manage overlay. Each card has a
* toggle (active on/off) and a model input (per-agent override).
* CEO is rendered but locked-on; clicking its toggle is a no-op.
*/
function renderCompanyAgentCards(payload) {
if (!_companyAgentList) return;
_companyAgentList.innerHTML = '';
if (_companyNameInput && payload && typeof payload.companyName === 'string') {
_companyNameInput.value = payload.companyName;
}
const agents = (payload && Array.isArray(payload.agents)) ? payload.agents : [];
for (const a of agents) {
const li = document.createElement('li');
li.className = 'company-agent-card';
li.setAttribute('data-active', a.active ? 'true' : 'false');
if (a.alwaysOn) li.setAttribute('data-locked', 'true');
const emoji = document.createElement('span');
emoji.className = 'company-agent-emoji';
emoji.textContent = a.emoji;
const body = document.createElement('div');
body.className = 'company-agent-body';
const name = document.createElement('div');
name.className = 'company-agent-name';
name.innerHTML = `${escAttr(a.name)} <span class="company-agent-role">${escAttr(a.role)}</span>`;
const tag = document.createElement('div');
tag.className = 'company-agent-tagline';
tag.textContent = a.tagline || '';
tag.title = a.specialty || '';
body.appendChild(name);
body.appendChild(tag);
const controls = document.createElement('div');
controls.className = 'company-agent-controls';
const modelInput = document.createElement('input');
modelInput.type = 'text';
modelInput.className = 'company-agent-model';
modelInput.placeholder = 'default';
modelInput.value = a.modelOverride || '';
modelInput.title = '비워두면 글로벌 기본 모델 사용';
modelInput.onchange = () => {
vscode.postMessage({
type: 'setCompanyAgentModel',
agentId: a.id,
model: modelInput.value.trim(),
});
};
const toggle = document.createElement('button');
toggle.className = 'company-agent-toggle';
toggle.textContent = a.active ? 'ON' : 'OFF';
if (a.alwaysOn) {
toggle.disabled = true;
toggle.textContent = 'LOCKED';
} else {
toggle.onclick = () => {
// Optimistic update + send the full new list so the
// backend has a single canonical replace operation.
const wantActive = !(li.getAttribute('data-active') === 'true');
li.setAttribute('data-active', wantActive ? 'true' : 'false');
toggle.textContent = wantActive ? 'ON' : 'OFF';
const nextIds = Array.from(_companyAgentList.querySelectorAll('.company-agent-card'))
.filter(el => el.getAttribute('data-active') === 'true')
.map(el => el.dataset.agentId)
.filter(Boolean);
vscode.postMessage({ type: 'setCompanyActiveAgents', value: nextIds });
};
}
li.dataset.agentId = a.id;
controls.appendChild(modelInput);
controls.appendChild(toggle);
li.appendChild(emoji);
li.appendChild(body);
li.appendChild(controls);
_companyAgentList.appendChild(li);
}
if (_companyStatusEl) _companyStatusEl.textContent = '';
}
/**
* Render one phase event from the dispatcher. The chat gets a
* card per phase so the user can follow progress in real time —
* "🧭 CEO 작업 분배 중..." → "📺 레오 작업 수행 중..." → final report.
*/
function renderCompanyPhase(ev) {
const chatEl = document.getElementById('chat');
if (!chatEl) return;
const card = document.createElement('div');
card.className = 'company-phase-card';
if (ev.phase === 'plan-start') {
card.innerHTML = '<div class="cph-head">🧭 CEO</div><div class="cph-meta">작업 분배 중…</div>';
} else if (ev.phase === 'plan-ready') {
const tasks = (ev.plan?.tasks || []).map((t, i) => `${i + 1}. <strong>${escAttr(t.agent)}</strong> — ${escAttr(t.task)}`).join('<br>');
card.innerHTML = `<div class="cph-head">🧭 CEO 브리프</div>
<div>${escAttr(ev.plan?.brief || '(brief 없음)')}</div>
<div class="cph-meta" style="margin-top:6px">${tasks || '(no tasks — chat reply)'}</div>`;
} else if (ev.phase === 'agent-start') {
card.innerHTML = `<div class="cph-head">${escAttr(ev.agentId)} 작업 수행 중…</div>
<div class="cph-meta">${escAttr(ev.task)} <em>(${ev.index + 1}/${ev.total})</em></div>`;
} else if (ev.phase === 'agent-done') {
const o = ev.output || {};
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 === 'report-start') {
card.innerHTML = '<div class="cph-head">🧭 CEO 종합 보고서 작성 중…</div>';
} else if (ev.phase === 'report-done') {
card.className += ' report';
card.innerHTML = `<div class="cph-head">🧭 CEO 보고서${ev.ok ? '' : ' (fallback)'}</div>
<div class="markdown-body">${fmt(ev.report || '')}</div>`;
} else if (ev.phase === 'session-saved') {
card.innerHTML = `<div class="cph-meta">세션 저장 완료 — 클릭하여 열기</div>`;
card.style.cursor = 'pointer';
card.onclick = () => vscode.postMessage({ type: 'openCompanySession', sessionDir: ev.sessionDir });
} else if (ev.phase === 'aborted') {
card.innerHTML = `<div class="cph-head">⛔ 회사 모드 중단</div><div class="cph-meta">${escAttr(ev.reason)}</div>`;
}
chatEl.appendChild(card);
chatEl.scrollTop = chatEl.scrollHeight;
}
// ── Knowledge Mix: global slider ──────────────────────────────────────
// Mirrors `g1nation.knowledgeMix.secondBrainWeight`. The hint label updates
// live as the user drags; the value is committed (postMessage) on `change`