release: v2.0.4 - Advanced Business Orchestration & UI Polishing

This commit is contained in:
g1nation
2026-05-13 23:32:29 +09:00
parent b6899851c3
commit 6784e85b7e
15 changed files with 417 additions and 45 deletions
@@ -1,5 +1,5 @@
{
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
"createdAt": 1778682078361,
"createdAt": 1778682718199,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
"createdAt": 1778682078352,
"createdAt": 1778682718198,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
"createdAt": 1778682078348,
"createdAt": 1778682718197,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "---\nid: stress_conflict_1778682078332\ndate: 2026-05-13T14:21:18.365Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (12ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (4ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (8ms)\n",
"createdAt": 1778682078365,
"result": "---\nid: stress_conflict_1778682718185\ndate: 2026-05-13T14:31:58.199Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (1ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (2ms)\n",
"createdAt": 1778682718199,
"modelVersion": "unknown"
}
@@ -1,8 +1,8 @@
{
"missionId": "stress_conflict_1778682078332",
"missionId": "stress_conflict_1778682718185",
"status": "completed",
"startTime": "2026-05-13T14:21:18.332Z",
"totalElapsedMs": 33,
"startTime": "2026-05-13T14:31:58.185Z",
"totalElapsedMs": 14,
"results": {
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
@@ -16,30 +16,30 @@
{
"from": "idle",
"to": "planner",
"durationMs": 12,
"durationMs": 11,
"message": "전략 수립 중...",
"ts": "2026-05-13T14:21:18.344Z"
"ts": "2026-05-13T14:31:58.196Z"
},
{
"from": "planner",
"to": "researcher",
"durationMs": 4,
"durationMs": 1,
"message": "핵심 정보 수집 및 분석 중...",
"ts": "2026-05-13T14:21:18.348Z"
"ts": "2026-05-13T14:31:58.197Z"
},
{
"from": "researcher",
"to": "writer",
"durationMs": 8,
"durationMs": 2,
"message": "최종 리포트 작성 및 편집 중...",
"ts": "2026-05-13T14:21:18.356Z"
"ts": "2026-05-13T14:31:58.199Z"
},
{
"from": "writer",
"to": "completed",
"durationMs": 9,
"durationMs": 0,
"message": "미션 완료",
"ts": "2026-05-13T14:21:18.365Z"
"ts": "2026-05-13T14:31:58.199Z"
}
],
"resilienceMetrics": {
+9
View File
@@ -1,5 +1,14 @@
# Astra Patch Notes
## v2.0.4 (2026-05-13)
### ⚡ Advanced Business Orchestration & UI Polishing
- **비즈니스 에이전트 고도화:** `companyConfig.ts``promptBuilder.ts` 수정을 통해 CEO 에이전트의 목표 설정 및 컨텍스트 주입 로직을 정교화했습니다.
- **사이드바 사용자 경험 개선:** `sidebar.css``sidebar.js`를 갱신하여 비즈니스 워크플로우 진행 상태 시각화를 최적화했습니다.
- **대화 핸들러 안정화:** `chatHandlers.ts``sidebarProvider.ts` 내의 비동기 메시지 처리 및 상태 동기화 로직을 강화했습니다.
- **신규 패키징:** `astra-2.0.4.vsix` 패키지를 통해 더욱 안정적인 비즈니스 자동화 환경을 제공합니다.
---
## v2.0.3 (2026-05-13)
### 🏢 AI 1-Person Company Engine & Business Intelligence
- **AI 1인 기업(Company) 엔진 도입:** 비즈니스 전략 수립부터 자동화 실행까지 아우르는 `src/features/company/` 모듈을 신규 도입했습니다.
+60 -1
View File
@@ -390,8 +390,67 @@
.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;
padding: 3px 6px; border-radius: 6px;
max-width: 150px; min-width: 0;
cursor: pointer;
}
.company-agent-model 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;
padding: 3px 6px; border-radius: 6px; cursor: pointer;
flex-shrink: 0;
}
.company-agent-edit:hover { color: var(--accent); border-color: var(--accent); }
.company-agent-edit.dirty {
color: var(--accent); border-color: var(--accent);
background: var(--accent-glow);
}
/* Expandable prompt editor under each agent card. Toggled via the
Edit button. Three textareas (tagline / specialty / persona) +
Reset / Save / Cancel — empty save clears that field's override. */
.company-agent-editor {
display: none;
margin: 6px 0 0 38px; /* indent under the emoji */
padding: 8px;
background: var(--bg-secondary);
border: 1px dashed var(--border);
border-radius: 6px;
}
.company-agent-card[data-expanded="true"] .company-agent-editor { display: block; }
.company-agent-editor .field-label {
display: flex; justify-content: space-between; align-items: center;
margin-top: 6px; font-size: 10px; color: var(--text-dim);
text-transform: uppercase; letter-spacing: 0.04em;
}
.company-agent-editor .field-label:first-child { margin-top: 0; }
.company-agent-editor .field-label .field-flag {
text-transform: none; letter-spacing: 0;
color: var(--accent); font-size: 9.5px;
}
.company-agent-editor input[type="text"],
.company-agent-editor textarea {
width: 100%; box-sizing: border-box;
background: var(--input-bg); color: var(--text-primary);
border: 1px solid var(--border); border-radius: 4px;
padding: 6px 8px; font-size: 11px; font-family: inherit;
margin-top: 3px;
}
.company-agent-editor textarea { resize: vertical; min-height: 60px; }
.company-agent-editor .editor-actions {
display: flex; justify-content: flex-end; gap: 6px; margin-top: 8px;
}
.company-agent-editor .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-editor .editor-actions button.primary {
background: var(--accent); border-color: var(--accent); color: #fff;
}
.company-agent-editor .editor-actions button.danger { color: var(--error); }
.company-agent-editor .editor-actions button:hover { border-color: var(--border-bright); }
/* Per-phase company turn header in chat. */
.company-phase-card {
+168 -18
View File
@@ -696,6 +696,13 @@
statusLabel.innerText = '';
// Refresh per-agent model dropdown options (if currently visible) so it stays in sync.
if (typeof refreshAgentMapModelOptions === 'function') refreshAgentMapModelOptions();
// If the company manage overlay is open with cached agent data,
// re-render its cards so each per-agent model <select> picks up
// any newly-discovered models from the master list.
if (typeof _lastCompanyAgentsPayload !== 'undefined' && _lastCompanyAgentsPayload
&& document.getElementById('companyOverlay')?.classList.contains('visible')) {
renderCompanyAgentCards(_lastCompanyAgentsPayload);
}
break;
}
case 'brainProfiles':
@@ -1430,12 +1437,53 @@
}
/**
* 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.
* Keep the last payload around so we can re-render whenever the
* model list refreshes (the top `#modelSel` is the source of truth
* for available models — see `populateAgentModelSelect`).
*/
let _lastCompanyAgentsPayload = null;
/**
* Populate one agent's model `<select>` from the master `#modelSel`
* options. Mirrors the pattern used by `refreshAgentMapModelOptions`
* so the user picks from the same canonical list everywhere.
* Empty value = "default (global)". Saved overrides not in the list
* are preserved as a "(saved)" option so the value never gets lost.
*/
function populateAgentModelSelect(sel, current) {
sel.innerHTML = '';
const useDefault = document.createElement('option');
useDefault.value = '';
useDefault.innerText = 'default (global)';
sel.appendChild(useDefault);
const seen = new Set();
for (const opt of modelSel.options) {
if (!opt.value || seen.has(opt.value)) continue;
seen.add(opt.value);
const o = document.createElement('option');
o.value = opt.value;
o.innerText = opt.innerText;
sel.appendChild(o);
}
if (current && !seen.has(current)) {
const o = document.createElement('option');
o.value = current;
o.innerText = `${current} (saved)`;
sel.appendChild(o);
}
sel.value = current || '';
}
/**
* Render the agent cards in the manage overlay. Each card has:
* - a model dropdown (default + every loaded model)
* - an ON/OFF toggle (CEO always-on)
* - an Edit button that toggles an inline prompt editor with
* tagline / specialty / persona textareas + Reset/Save/Cancel.
*/
function renderCompanyAgentCards(payload) {
if (!_companyAgentList) return;
_lastCompanyAgentsPayload = payload;
_companyAgentList.innerHTML = '';
if (_companyNameInput && payload && typeof payload.companyName === 'string') {
_companyNameInput.value = payload.companyName;
@@ -1446,6 +1494,14 @@
li.className = 'company-agent-card';
li.setAttribute('data-active', a.active ? 'true' : 'false');
if (a.alwaysOn) li.setAttribute('data-locked', 'true');
li.dataset.agentId = a.id;
// ── Row 1: emoji + name/tagline + controls ──
const row = document.createElement('div');
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.gap = '10px';
row.style.width = '100%';
const emoji = document.createElement('span');
emoji.className = 'company-agent-emoji';
@@ -1466,20 +1522,30 @@
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 = () => {
const modelSelEl = document.createElement('select');
modelSelEl.className = 'company-agent-model';
modelSelEl.title = '비워두면 글로벌 기본 모델 사용';
populateAgentModelSelect(modelSelEl, a.modelOverride || '');
modelSelEl.onchange = () => {
vscode.postMessage({
type: 'setCompanyAgentModel',
agentId: a.id,
model: modelInput.value.trim(),
model: modelSelEl.value || '',
});
};
const editBtn = document.createElement('button');
editBtn.className = 'company-agent-edit';
editBtn.textContent = '✎ Edit';
if (a.personaOverridden || a.specialtyOverridden || a.taglineOverridden) {
editBtn.classList.add('dirty');
editBtn.title = 'prompt 편집됨 (원본과 다름)';
}
editBtn.onclick = () => {
const expanded = li.getAttribute('data-expanded') === 'true';
li.setAttribute('data-expanded', expanded ? 'false' : 'true');
};
const toggle = document.createElement('button');
toggle.className = 'company-agent-toggle';
toggle.textContent = a.active ? 'ON' : 'OFF';
@@ -1488,8 +1554,6 @@
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';
@@ -1500,18 +1564,104 @@
vscode.postMessage({ type: 'setCompanyActiveAgents', value: nextIds });
};
}
li.dataset.agentId = a.id;
controls.appendChild(modelInput);
controls.appendChild(modelSelEl);
controls.appendChild(editBtn);
controls.appendChild(toggle);
li.appendChild(emoji);
li.appendChild(body);
li.appendChild(controls);
row.appendChild(emoji);
row.appendChild(body);
row.appendChild(controls);
li.appendChild(row);
// ── Row 2 (collapsed by default): prompt editor ──
li.appendChild(_buildAgentPromptEditor(a));
_companyAgentList.appendChild(li);
}
if (_companyStatusEl) _companyStatusEl.textContent = '';
}
/**
* Build the per-agent prompt editor. Hidden until the user clicks
* the Edit button. Three fields (tagline / specialty / persona);
* Save sends whichever fields actually changed; Reset sends `null`
* to wipe all overrides at once.
*/
function _buildAgentPromptEditor(a) {
const editor = document.createElement('div');
editor.className = 'company-agent-editor';
const _field = (key, labelText, isTextarea, current, defaultVal, overridden) => {
const lbl = document.createElement('label');
lbl.className = 'field-label';
lbl.innerHTML = `<span>${labelText}</span>` +
(overridden
? '<span class="field-flag">overridden</span>'
: '<span class="field-flag" style="color:var(--text-dim)">default</span>');
editor.appendChild(lbl);
const el = isTextarea
? document.createElement('textarea')
: document.createElement('input');
if (!isTextarea) el.type = 'text';
el.value = current || '';
el.placeholder = defaultVal || '';
editor.appendChild(el);
return el;
};
const tagInput = _field('tagline', 'Tagline (한 줄)', false, a.tagline, a.defaultTagline, a.taglineOverridden);
const specInput = _field('specialty', 'Specialty (CEO가 dispatch 판단에 사용)', true, a.specialty, a.defaultSpecialty, a.specialtyOverridden);
const persInput = _field('persona', 'Persona (말투·관점·강조)', true, a.persona, a.defaultPersona, a.personaOverridden);
specInput.rows = 3;
persInput.rows = 5;
const actions = document.createElement('div');
actions.className = 'editor-actions';
const resetBtn = document.createElement('button');
resetBtn.className = 'danger';
resetBtn.textContent = 'Reset';
resetBtn.title = '이 에이전트의 모든 override 제거 → 디폴트로 복귀';
resetBtn.onclick = () => {
vscode.postMessage({
type: 'setCompanyAgentPrompt',
agentId: a.id,
override: null,
});
};
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
cancelBtn.onclick = () => {
const card = editor.closest('.company-agent-card');
if (card) card.setAttribute('data-expanded', 'false');
};
const saveBtn = document.createElement('button');
saveBtn.className = 'primary';
saveBtn.textContent = 'Save';
saveBtn.onclick = () => {
// Send what's currently in each field. The backend treats an
// empty string as "clear this field" (back to default), so
// typing nothing into Tagline + saving = Tagline default,
// Specialty + Persona untouched if not modified.
vscode.postMessage({
type: 'setCompanyAgentPrompt',
agentId: a.id,
override: {
tagline: tagInput.value === a.defaultTagline ? '' : tagInput.value,
specialty: specInput.value === a.defaultSpecialty ? '' : specInput.value,
persona: persInput.value === a.defaultPersona ? '' : persInput.value,
},
});
};
actions.appendChild(resetBtn);
actions.appendChild(cancelBtn);
actions.appendChild(saveBtn);
editor.appendChild(actions);
return editor;
}
/**
* Render one phase event from the dispatcher. The chat gets a
* card per phase so the user can follow progress in real time —
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "astra",
"displayName": "Astra",
"description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.",
"version": "2.0.3",
"version": "2.0.4",
"publisher": "g1nation",
"license": "MIT",
"icon": "assets/icon.png",
+90 -2
View File
@@ -20,7 +20,7 @@
*/
import * as vscode from 'vscode';
import { COMPANY_AGENTS, DEFAULT_ACTIVE_AGENTS, getCompanyAgent } from './agents';
import { CompanyState, COMPANY_STATE_KEY } from './types';
import { AgentPromptOverride, CompanyState, COMPANY_STATE_KEY } from './types';
/** Default state for a brand-new user. CEO is always on. */
function _defaultState(): CompanyState {
@@ -29,6 +29,7 @@ function _defaultState(): CompanyState {
companyName: '1인 기업',
activeAgentIds: DEFAULT_ACTIVE_AGENTS.slice(),
modelOverrides: {},
promptOverrides: {},
};
}
@@ -57,7 +58,29 @@ function _normalize(raw: Partial<CompanyState> | undefined): CompanyState {
}
}
}
return { enabled, companyName, activeAgentIds: withoutCeo, modelOverrides: overrides };
// Prompt overrides — drop anything for an unknown agent, and prune empty
// sub-fields so we never persist `{persona: ""}` (resolveAgentPrompt
// treats empty same as missing, but cleaner state stays portable).
const promptOverrides: Record<string, AgentPromptOverride> = {};
if (raw.promptOverrides && typeof raw.promptOverrides === 'object') {
for (const [agentId, v] of Object.entries(raw.promptOverrides as Record<string, unknown>)) {
if (!getCompanyAgent(agentId) || !v || typeof v !== 'object') continue;
const ov = v as Record<string, unknown>;
const cleaned: AgentPromptOverride = {};
if (typeof ov.persona === 'string' && ov.persona.trim()) cleaned.persona = ov.persona.trim();
if (typeof ov.specialty === 'string' && ov.specialty.trim()) cleaned.specialty = ov.specialty.trim();
if (typeof ov.tagline === 'string' && ov.tagline.trim()) cleaned.tagline = ov.tagline.trim();
if (cleaned.persona || cleaned.specialty || cleaned.tagline) {
promptOverrides[agentId] = cleaned;
}
}
}
return {
enabled, companyName,
activeAgentIds: withoutCeo,
modelOverrides: overrides,
promptOverrides,
};
}
/** Read the current company state. Always returns a fully-populated object. */
@@ -134,6 +157,48 @@ export async function setAgentModelOverride(
return next;
}
/**
* Set / clear a per-agent prompt override. Pass `null` (or an empty
* `AgentPromptOverride`) to drop the override entirely; pass a partial
* object to update just the named fields while keeping the others.
*
* Field semantics:
* - non-empty string → save as the new override
* - empty string / null / undefined → *clear* that sub-field
* This makes "save just persona" and "reset just specialty" symmetric: send
* the new persona text and an empty string for specialty.
*/
export async function setAgentPromptOverride(
context: vscode.ExtensionContext,
agentId: string,
override: AgentPromptOverride | null,
): Promise<CompanyState> {
const cur = readCompanyState(context);
const overrides = { ...cur.promptOverrides };
if (!override) {
delete overrides[agentId];
} else {
const existing: AgentPromptOverride = { ...(overrides[agentId] ?? {}) };
for (const key of ['persona', 'specialty', 'tagline'] as const) {
const v = override[key];
if (v === undefined) continue; // not specified → leave as-is
if (typeof v === 'string' && v.trim()) {
existing[key] = v.trim();
} else {
delete existing[key];
}
}
if (existing.persona || existing.specialty || existing.tagline) {
overrides[agentId] = existing;
} else {
delete overrides[agentId];
}
}
const next: CompanyState = { ...cur, promptOverrides: overrides };
await writeCompanyState(context, next);
return next;
}
// ── Derived helpers (no I/O) ────────────────────────────────────────────────
/**
@@ -177,6 +242,29 @@ export function summarizeForChip(state: CompanyState): string {
return `${state.companyName} · ${count} agents`;
}
/**
* Resolve the *effective* prompt fields for an agent — merge the static
* default from `agents.ts` with any user-saved override. Returns plain
* strings so the prompt builder doesn't have to worry about which source
* each field came from.
*/
export function resolveAgentPrompt(state: CompanyState, agentId: string): {
persona: string;
specialty: string;
tagline: string;
/** Whether *any* field is currently overridden — useful for UI hints. */
hasOverride: boolean;
} {
const def = getCompanyAgent(agentId);
const ov = state.promptOverrides?.[agentId];
return {
persona: (ov?.persona ?? def?.persona ?? '').toString(),
specialty: (ov?.specialty ?? def?.specialty ?? '').toString(),
tagline: (ov?.tagline ?? def?.tagline ?? '').toString(),
hasOverride: !!(ov && (ov.persona || ov.specialty || ov.tagline)),
};
}
// Re-export the static catalogue so callers only have to import from one
// module to get the full picture.
export { COMPANY_AGENTS, getCompanyAgent };
+3
View File
@@ -20,6 +20,8 @@ export {
setCompanyName,
setActiveAgents,
setAgentModelOverride,
setAgentPromptOverride,
resolveAgentPrompt,
activeAgentIds,
isAgentActive,
modelForAgent,
@@ -31,6 +33,7 @@ export type {
CompanyState,
CompanyTaskPlan,
AgentTurnOutput,
AgentPromptOverride,
SessionResult,
} from './types';
+8 -3
View File
@@ -16,6 +16,7 @@
* memory/decisions and passes them in), which keeps it trivial to test.
*/
import { COMPANY_AGENTS, getCompanyAgent } from './agents';
import { resolveAgentPrompt } from './companyConfig';
import { CompanyState } from './types';
export interface SpecialistPromptInputs {
@@ -50,16 +51,20 @@ export function buildSpecialistPrompt(inputs: SpecialistPromptInputs): string {
return `You are an agent named "${inputs.agentId}". Respond in Korean.`;
}
const company = inputs.state.companyName || '1인 기업';
// Honour any per-agent prompt overrides the user saved in the manage
// panel. `resolveAgentPrompt` falls back to the static defaults from
// `agents.ts` for fields the user hasn't edited.
const resolved = resolveAgentPrompt(inputs.state, inputs.agentId);
const parts: string[] = [];
// ── Identity ──
parts.push(`# ${agent.emoji} ${agent.name}${agent.role}`);
parts.push(`당신은 ${company}${agent.role}입니다.`);
parts.push(`전문 분야: ${agent.specialty}`);
if (agent.persona) {
parts.push(`전문 분야: ${resolved.specialty}`);
if (resolved.persona) {
parts.push('');
parts.push('## 페르소나');
parts.push(agent.persona);
parts.push(resolved.persona);
}
// ── Output contract ──
+23
View File
@@ -34,6 +34,22 @@ export interface CompanyAgentDef {
alwaysOn?: boolean;
}
/**
* User edits to an agent's *prompt* surface. Each field is optional: empty
* or missing means "use the static default from `agents.ts`". Storing this
* in state (rather than rewriting `agents.ts`) means the user can re-tune
* any agent's voice / specialty without touching code, and a `Reset` button
* can wipe the override to recover the shipped default.
*/
export interface AgentPromptOverride {
/** Replaces the persona block. Multi-line markdown allowed. */
persona?: string;
/** Replaces the specialty list shown to the CEO planner + specialist. */
specialty?: string;
/** Replaces the short tagline shown in the manage UI. */
tagline?: string;
}
/**
* Persisted runtime state for the company mode. Stored in VS Code's
* `globalState` plus a small JSON file under `.astra/company/_shared/`.
@@ -53,6 +69,13 @@ export interface CompanyState {
* time, by design.
*/
modelOverrides: Record<string, string>;
/**
* Optional per-agent prompt edits (persona / specialty / tagline).
* Empty record = every agent uses its shipped default. The dispatcher
* resolves the effective values via `resolveAgentPrompt` at build time
* so changes apply on the very next turn — no restart required.
*/
promptOverrides: Record<string, AgentPromptOverride>;
}
/** Output of the CEO planner LLM call after JSON parsing. */
+20
View File
@@ -198,6 +198,26 @@ export async function handleChatMessage(provider: SidebarChatProvider, data: any
}
return true;
}
case 'setCompanyAgentPrompt': {
// Patch one agent's persona / specialty / tagline. Each field is
// optional in the payload; passing an *empty string* explicitly
// clears that field (back to the default from `agents.ts`).
// Sending `null` for the whole override resets every field at once.
const { setAgentPromptOverride } = await import('../features/company');
const agentId = typeof data.agentId === 'string' ? data.agentId : '';
if (!agentId) return true;
const v = data.override;
const override = v === null
? null
: {
persona: typeof v?.persona === 'string' ? v.persona : undefined,
specialty: typeof v?.specialty === 'string' ? v.specialty : undefined,
tagline: typeof v?.tagline === 'string' ? v.tagline : undefined,
};
await setAgentPromptOverride(provider._context, agentId, override);
await provider._sendCompanyAgents();
return true;
}
case 'proactiveTrigger':
await provider._handleProactiveSuggestion(data.context);
return true;
+19 -4
View File
@@ -1215,24 +1215,39 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
});
}
/** Push the full agent catalogue when the manage panel opens. */
/**
* Push the full agent catalogue when the manage panel opens. Each entry
* carries both the *default* (from `agents.ts`) and *override* (from
* globalState) fields so the UI can show the user what they've edited,
* gray out unchanged fields, and offer a Reset button per agent.
*/
async _sendCompanyAgents(): Promise<void> {
if (!this._view) return;
const state = readCompanyState(this._context);
const agents = COMPANY_AGENT_ORDER.map((id) => {
const def = COMPANY_AGENTS[id];
const override = state.promptOverrides[id] || {};
return {
id,
name: def.name,
role: def.role,
emoji: def.emoji,
color: def.color,
tagline: def.tagline,
specialty: def.specialty,
hasPersona: !!def.persona,
alwaysOn: !!def.alwaysOn,
active: id === 'ceo' || state.activeAgentIds.includes(id),
modelOverride: state.modelOverrides[id] || '',
// Defaults — never change at runtime.
defaultTagline: def.tagline,
defaultSpecialty: def.specialty,
defaultPersona: def.persona || '',
// Current effective values (default + override merged).
tagline: override.tagline || def.tagline,
specialty: override.specialty || def.specialty,
persona: override.persona || def.persona || '',
// Per-field override flags for the UI.
personaOverridden: !!override.persona,
specialtyOverridden: !!override.specialty,
taglineOverridden: !!override.tagline,
};
});
this._view.webview.postMessage({