diff --git a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json index 43c00fb..e971f8e 100644 --- a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json +++ b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json @@ -1,5 +1,5 @@ { "result": "Final report with inconsistencies. This should be long enough to pass validation.", - "createdAt": 1778682078361, + "createdAt": 1778682718199, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json index 9b5c615..89fce0f 100644 --- a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json +++ b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json @@ -1,5 +1,5 @@ { "result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", - "createdAt": 1778682078352, + "createdAt": 1778682718198, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json index d737ebe..242d4c8 100644 --- a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json +++ b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json @@ -1,5 +1,5 @@ { "result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", - "createdAt": 1778682078348, + "createdAt": 1778682718197, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json index 8a88815..4b1da63 100644 --- a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json +++ b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json @@ -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" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/missions/stress_conflict_1778682078332.json b/.astra/tests/stress/.astra/missions/stress_conflict_1778682718185.json similarity index 78% rename from .astra/tests/stress/.astra/missions/stress_conflict_1778682078332.json rename to .astra/tests/stress/.astra/missions/stress_conflict_1778682718185.json index 9bb1590..0558b24 100644 --- a/.astra/tests/stress/.astra/missions/stress_conflict_1778682078332.json +++ b/.astra/tests/stress/.astra/missions/stress_conflict_1778682718185.json @@ -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": { diff --git a/PATCHNOTES.md b/PATCHNOTES.md index a562ed6..1002ca6 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -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/` 모듈을 신규 도입했습니다. diff --git a/media/sidebar.css b/media/sidebar.css index dd325fb..2620a85 100644 --- a/media/sidebar.css +++ b/media/sidebar.css @@ -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 { diff --git a/media/sidebar.js b/media/sidebar.js index c37635b..7fe2007 100644 --- a/media/sidebar.js +++ b/media/sidebar.js @@ -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 ` 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 = `${labelText}` + + (overridden + ? 'overridden' + : 'default'); + 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 — diff --git a/package.json b/package.json index 0afb3c0..1ccf2db 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/features/company/companyConfig.ts b/src/features/company/companyConfig.ts index 4c21f30..a9212eb 100644 --- a/src/features/company/companyConfig.ts +++ b/src/features/company/companyConfig.ts @@ -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 | 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 = {}; + if (raw.promptOverrides && typeof raw.promptOverrides === 'object') { + for (const [agentId, v] of Object.entries(raw.promptOverrides as Record)) { + if (!getCompanyAgent(agentId) || !v || typeof v !== 'object') continue; + const ov = v as Record; + 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 { + 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 }; diff --git a/src/features/company/index.ts b/src/features/company/index.ts index 77e4513..7c40f2b 100644 --- a/src/features/company/index.ts +++ b/src/features/company/index.ts @@ -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'; diff --git a/src/features/company/promptBuilder.ts b/src/features/company/promptBuilder.ts index 2d56c47..af9ba8f 100644 --- a/src/features/company/promptBuilder.ts +++ b/src/features/company/promptBuilder.ts @@ -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 ── diff --git a/src/features/company/types.ts b/src/features/company/types.ts index 643a035..1f88be0 100644 --- a/src/features/company/types.ts +++ b/src/features/company/types.ts @@ -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; + /** + * 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; } /** Output of the CEO planner LLM call after JSON parsing. */ diff --git a/src/sidebar/chatHandlers.ts b/src/sidebar/chatHandlers.ts index a99798d..fda1fe4 100644 --- a/src/sidebar/chatHandlers.ts +++ b/src/sidebar/chatHandlers.ts @@ -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; diff --git a/src/sidebarProvider.ts b/src/sidebarProvider.ts index 546a763..75e6024 100644 --- a/src/sidebarProvider.ts +++ b/src/sidebarProvider.ts @@ -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 { 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({