release: v2.0.3 - AI 1-Person Company Engine & Business Intelligence
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user