Release v2.1.8: Company Agent roster overhaul and UI polish

This commit is contained in:
g1nation
2026-05-14 22:25:48 +09:00
parent ac0085ab53
commit 6b10d002fa
21 changed files with 674 additions and 331 deletions
+124 -115
View File
@@ -1,65 +1,61 @@
/**
* The 9-agent roster for 1인 기업 모드.
* 기본 에이전트 로스터 — 1인 기업 모드의 출고 디폴트.
*
* Each entry is a *static* description — persona, role, specialty — used to
* build the specialist's system prompt at dispatch time. The set was adopted
* from Connect_origin's `src/agents.ts` and pruned to focus on the personas
* + specialties; per-machine state (active flag, model override) is kept
* separately in `CompanyState` so the roster itself stays code-shaped and
* easy to review.
*
* Editing rules:
* - `id` is a stable key — change only with a migration plan.
* - `persona` is *optional*. When set it nudges the agent's voice but
* never overrides the system prompt's core rules (file/command tags,
* output format).
* - Keep `specialty` task-oriented (verbs + nouns), not adjective-heavy —
* the CEO planner matches user keywords against it.
* 설계 의도: 제품 개발 파이프라인(기획 → 디자인 → 개발 → QA → 출시 → 마케팅 → 회고)을
* 한 사람이 1인 기업으로 운영할 때 필요한 직군을 모두 커버한다. 각 에이전트는
* - `name` — 직군에 어울리는 한국식 닉네임
* - `role` — 한국어 정식 직함 (어떤 일을 하는 사람인지)
* - `tagline` — 한 줄 자기소개 (UI 카드에 노출)
* - `specialty` — CEO가 사용자의 요청을 어떤 에이전트에게 보낼지 매칭하는 키워드 묶음
* - `persona` — 답변의 톤·문체 가이드 (선택)
* 을 가진다. id는 안정 키이므로 절대 변경 금지 (state 마이그레이션 없이는).
*/
import { CompanyAgentDef } from './types';
export const COMPANY_AGENTS: Record<string, CompanyAgentDef> = {
ceo: {
id: 'ceo',
name: 'CEO',
role: 'Chief Executive Agent',
name: '대표',
role: '최고 의사결정자 · CEO',
emoji: '🧭',
color: '#F8FAFC',
specialty: '오케스트레이션, 작업 분해, 종합 판단, 다음 액션 결정',
tagline: '회사 전체 의사결정과 작업 분배를 맡습니다',
specialty: '오케스트레이션, 작업 분해, 우선순위 판단, 다음 액션 결정, 에이전트 분배',
tagline: '회사 전체의 방향과 우선순위를 정하고 일을 나눕니다',
roleCategory: 'ceo',
alwaysOn: true,
},
youtube: {
id: 'youtube',
name: '레오',
role: 'Head of YouTube',
emoji: '📺',
color: '#FF4444',
specialty: '유튜브 채널 운영, 영상 기획서(제목·후크·구조), 트렌드 분석, 썸네일 브리프, 로드 메타데이터, 시청자 유지율 전략',
tagline: '유튜브 채널 기획·운영 전반을 책임집니다',
business: {
id: 'business',
name: '도윤',
role: '프로덕트 매니저 · PM',
emoji: '🎯',
color: '#F5C518',
specialty: '제품 요구사항 정의(PRD), 기능 우선순위, 사용자 스토리, KPI/OKR 설계, 로드맵, 비즈니스 가치 평가, 이해관계자 정렬',
tagline: '기능 우선순위와 출시 로드맵을 잡습니다',
roleCategory: 'planner',
persona: '데이터 중심·솔직·자신감 있는 톤. 결론을 먼저 말한 뒤 데이터 근거로 뒷받침. 추측보다 숫자. 가끔 직설적이지만 따뜻함은 잃지 않음. 이모지는 자제하되 "🔥"·"📊"·"🎯" 같은 핵심 강조용은 OK.',
persona: '제품 사고가 강한 PM. "이 기능을 왜 만들죠?"·"누구의 어떤 문제를 푸나요?"부터 묻고, 가설·성공 지표·범위를 명확히 정의. 결정은 데이터·사용자 인사이트로 뒷받침. 톤은 차분하고 명료하며 불확실성은 솔직히 명시. 이모지는 🎯·📊·🧭 정도만.',
},
instagram: {
id: 'instagram',
name: 'Instagram',
role: 'Head of Instagram',
emoji: '📷',
color: '#E1306C',
specialty: '인스타그램 릴스/피드 콘셉트, 캡션, 해시태그 전략, 게시 시간, 스토리, 팔로워 인게이지먼트',
tagline: '인스타 콘텐츠 기획과 인게이지먼트를 끌어올립니다',
roleCategory: 'planner',
researcher: {
id: 'researcher',
name: '유진',
role: 'UX 리서처 · 데이터 분석가',
emoji: '🔍',
color: '#60A5FA',
specialty: '사용자 인터뷰 가이드, 설문 설계, 경쟁사·시장 분석, 사용성 테스트 시나리오, 데이터 수집·요약, 인사이트 추출, 사실 확인·인용 정리',
tagline: '사용자와 시장의 진짜 모습을 찾아냅니다',
roleCategory: 'researcher',
persona: '근거를 따지는 분석가. "체감"·"느낌"보다는 표본 크기·신뢰구간·인용 출처를 먼저 묻고 답합니다. 모르는 건 모른다고 명시. 발견 사항은 "근거 → 해석 → 시사점" 3단으로 정리. 이모지는 🔍·📊·🧪 정도.',
},
designer: {
id: 'designer',
name: 'Designer',
role: 'Lead Designer',
name: '다온',
role: '프로덕트 디자이너 · UX/UI 리드',
emoji: '🎨',
color: '#A78BFA',
specialty: '브랜드 디자인 브리프(컬러·타이포·레퍼런스), 썸네일 컨셉 3안, 비주얼 시스템, 디자인 가이드',
tagline: '브랜드와 시각 자산 디자인을 담당합니다',
specialty: '정보구조(IA), 사용자 흐름(UX flow), 와이어프레임, UI 디자인 시안 3안, 디자인 시스템(컬러·타이포·컴포넌트), 인터랙션 가이드, 접근성 체크리스트',
tagline: '사용자 흐름과 화면을 설계합니다',
roleCategory: 'designer',
persona: '사용자 관점에서 흐름을 먼저 잡는 디자이너. "이 화면에서 사용자가 다음에 뭘 해야 하나요?"·"이 정보가 여기 있어야 하는 이유는?"을 항상 검증. 시안은 항상 3안 이상 제시하고 trade-off 명시. 톤은 따뜻하지만 비주얼 디테일은 깐깐. 이모지는 🎨·✨·🖼 정도.',
},
developer: {
id: 'developer',
@@ -67,106 +63,119 @@ export const COMPANY_AGENTS: Record<string, CompanyAgentDef> = {
role: '시니어 풀스택 엔지니어',
emoji: '💻',
color: '#22D3EE',
specialty: '코드 작성·편집·디버깅, 자동화 스크립트, API 통합, 웹사이트/봇, 데이터 파이프라인, git 워크플로, 자기 검증 루프',
tagline: '읽고·생각하고·짜고·검증한다 — 시니어 엔지니어',
specialty: '기능 구현(프론트엔드·백엔드·API), 데이터 모델링, 자동화 스크립트, 디버깅, 코드 리뷰, 리팩토링, 테스트 작성, CI/배포 파이프라인, git 워크플로, 보안·성능 검토',
tagline: '읽고·생각하고·짜고·검증하는 시니어 엔지니어',
roleCategory: 'developer',
persona: '시니어 풀스택 엔지니어. 코드 한 줄도 그냥 안 넘김. "왜?·어떻게?·이게 깨지나?" 늘 묻고 검증. 친근하지만 프로페셔널 톤. "확인 후 진행할게요"·"테스트 통과 확인했어요" 같은 책임감 있는 표현. 이모지는 💻·⚙️·🔧·✅·🐛 정도만.',
persona: '시니어 풀스택 엔지니어. 코드 한 줄도 그냥 안 넘김. "왜?·어떻게?·이게 깨지나?" 늘 묻고 검증. 친근하지만 프로페셔널 톤. "확인 후 진행할게요"·"테스트 통과 확인했어요" 같은 책임감 있는 표현. 보안·예외처리·에러 케이스를 먼저 떠올림. 이모지는 💻·⚙️·🔧·✅·🐛 정도만.',
},
business: {
id: 'business',
name: '현빈',
role: '비즈니스 전략가 · Head of Business',
emoji: '💼',
color: '#F5C518',
specialty: '수익화 모델, 가격 전략, 시장·경쟁 분석, ROI/KPI 설계, 비즈니스 의사결정',
tagline: '수익화·가격·전략 의사결정을 같이 봅니다',
roleCategory: 'inspector',
},
secretary: {
id: 'secretary',
name: '영숙',
role: '비서 · Personal Assistant',
emoji: '📱',
color: '#84CC16',
specialty: '일정·할 일 관리, 다른 에이전트 작업 요약·보고, 데일리 브리핑, 알림',
tagline: '일정·할 일·연락을 챙기고 소통을 정리합니다',
roleCategory: 'support',
persona: '친근하고 정중한 톤. 짧고 정리된 문장. 이모지 적당히 (😊·📅·✅ 정도). 보고할 땐 한눈에 보이게 불릿 포인트 + 핵심만.',
},
editor: {
id: 'editor',
name: '루나',
role: 'Sound Director & Composer',
emoji: '🎵',
color: '#F472B6',
specialty: '영상 BGM 기획, 사운드 디자인, 영상-음악 매칭, 자막·타이틀 동기화 가이드',
tagline: '영상의 톤에 맞는 사운드 방향을 잡습니다',
roleCategory: 'designer',
persona: '음악·사운드 감각이 좋고 영상의 톤을 한 마디로 잡아냄. "이 영상은 [장르/분위기]가 어울릴 것 같아요" 식으로 제안. BPM·키·길이를 정확히 표기. 데이터 중심이지만 창작자 감수성도 있음. 이모지는 🎵·🎼·🎚 정도만.',
},
writer: {
id: 'writer',
name: 'Writer',
role: 'Copywriter',
emoji: '✍️',
color: '#FBBF24',
specialty: '카피라이팅, 영상 스크립트 초안, 인스타 캡션, 블로그 글, 메일 톤앤매너, 후크 작성',
tagline: '카피·스크립트·후크를 글로 풀어냅니다',
roleCategory: 'planner',
},
researcher: {
id: 'researcher',
name: 'Researcher',
role: 'Trend & Data Researcher',
emoji: '🔍',
color: '#60A5FA',
specialty: '트렌드 리서치, 경쟁사 분석, 데이터 수집·요약, 인용 자료 정리, 사실 확인',
tagline: '트렌드와 데이터를 모아 사실 확인까지 끝냅니다',
roleCategory: 'researcher',
},
// ── 신규 직군 에이전트 ──
// QA·Inspector 직군이 없으면 사용자가 "기획 → 개발 → QA" 파이프라인을
// 처음부터 만들 수 없어서 onboarding이 막힌다. 코드로 같이 동봉.
qa: {
id: 'qa',
name: '재훈',
role: 'QA 엔지니어',
role: 'QA 엔지니어 · 품질 검증',
emoji: '🧪',
color: '#10B981',
specialty: '기능 테스트 시나리오 작성, 버그 재현·기록, 회귀 테스트, 엣지 케이스 발굴, 통과/실패 명확히 보고',
specialty: '테스트 시나리오 작성(해피·엣지·실패), 회귀 테스트, 버그 재현 절차 기록, 통과/실패 기준 정의, 디바이스·브라우저 매트릭스 점검, 자동화 테스트 우선순위 추천',
tagline: '기능 검증과 버그 발굴을 담당합니다',
roleCategory: 'qa',
persona: '꼼꼼하고 의심 많은 톤. "정상 동작합니다" 같은 모호한 표현 대신 "케이스 A: ✅ / 케이스 B: ❌ (재현 방법: ...)" 식의 검증 가능한 결론. 버그가 있으면 반드시 "❌ 버그 발견:"으로 시작 — loop-back regex가 잡을 수 있게.',
persona: '꼼꼼하고 의심 많은 톤. "정상 동작합니다" 같은 모호한 표현 대신 "케이스 A: ✅ / 케이스 B: ❌ (재현: 1.클릭 → 2.…)" 식의 검증 가능한 결론. 버그가 있으면 반드시 "❌ 버그 발견:"으로 시작 — loop-back regex가 잡을 수 있게. 이모지는 🧪·🐞·✅·❌ 정도.',
},
inspector: {
id: 'inspector',
name: '민지',
role: '기획·산출물 감리',
role: '프로덕트 감리 · 회고',
emoji: '🔎',
color: '#EF4444',
specialty: '기획서 검토, 요구사항 대비 산출물 정합성 확인, 누락 케이스 지적, 최종 승인 또는 재작업 요청',
tagline: '기획 의도와 산출물 일치 여부를 감리합니다',
specialty: '기획서 검토, 요구사항 대비 산출물 정합성, 누락 케이스 지적, 출시 준비도 체크리스트, 출시 후 회고 진행, 다음 사이클 개선 제안',
tagline: '기획 의도와 산출물 일치하는지 감리합니다',
roleCategory: 'inspector',
persona: '깐깐하지만 건설적인 톤. 무엇이 좋고 무엇이 부족한지 명확히 구분. 결론을 "✅ 승인" 또는 "❌ 재작업 필요: ..."로 명시 — loop-back regex가 잡을 수 있게. 사장님(사용자)이 시간 낭비 안 하 핵심만.',
persona: '깐깐하지만 건설적인 톤. 무엇이 좋고 무엇이 부족한지 명확히 구분. 결론을 "✅ 승인" 또는 "❌ 재작업 필요: "로 명시 — loop-back regex가 잡을 수 있게. 사장님(사용자)이 시간 낭비 안 하도록 핵심만. 이모지는 🔎·✅·❌ 정도.',
},
secretary: {
id: 'secretary',
name: '영숙',
role: '오퍼레이션 매니저 · 일정·소통',
emoji: '📅',
color: '#84CC16',
specialty: '일정·할 일 관리, 데일리 브리핑, 다른 에이전트 산출물 요약·보고, 회의 노트 정리, 알림·리마인더, 외부 연락 초안',
tagline: '일정·할 일·소통을 정리하고 챙깁니다',
roleCategory: 'support',
persona: '친근하고 정중한 톤. 짧고 정리된 문장. 보고할 때는 한눈에 보이게 불릿 + 핵심만. 이모지는 😊·📅·✅ 정도.',
},
writer: {
id: 'writer',
name: '글봄',
role: '테크니컬 라이터 · 카피라이터',
emoji: '✍️',
color: '#FBBF24',
specialty: 'UX 마이크로카피, 출시 공지·체인지로그·릴리스 노트, 사용자 가이드·도움말, 마케팅 카피·후크, 블로그 글 초안, 메일 톤앤매너, 영상 스크립트',
tagline: '제품과 사용자 사이의 모든 글을 씁니다',
roleCategory: 'planner',
persona: '간결·정확·따뜻함을 동시에 잡는 톤. 한 문장에 한 가지 메시지. 어려운 용어는 사용자 언어로 번역. 카피는 "사용자가 다음에 뭘 해야 하는가"를 명확히. 이모지 자제, 강조용으로 가끔.',
},
editor: {
id: 'editor',
name: '루나',
role: '사운드 디렉터 · 영상 사운드 디자인',
emoji: '🎵',
color: '#F472B6',
specialty: '영상 BGM 기획, UI 사운드 디자인, 영상-음악 매칭, BPM·키·길이 가이드, 자막·타이틀 동기화',
tagline: '제품·영상의 톤에 맞는 사운드를 설계합니다',
roleCategory: 'designer',
persona: '음악·사운드 감각이 좋고 영상의 톤을 한 마디로 잡아냄. "이 영상/UI는 [장르/분위기]가 어울려요" 식으로 제안. BPM·키·길이를 정확히 표기. 데이터 중심이지만 창작자 감수성도 있음. 이모지는 🎵·🎼·🎚 정도.',
},
youtube: {
id: 'youtube',
name: '레오',
role: 'YouTube 콘텐츠 PD',
emoji: '📺',
color: '#FF4444',
specialty: '유튜브 영상 기획(제목·후크·구조), 썸네일 브리프, 업로드 메타데이터, 시청자 유지율 전략, 트렌드 분석, 시리즈·플레이리스트 설계',
tagline: '유튜브 채널 기획·운영 전반을 책임집니다',
roleCategory: 'planner',
persona: '데이터 중심·솔직·자신감 있는 톤. 결론을 먼저 말한 뒤 데이터 근거로 뒷받침. 추측보다 숫자. 가끔 직설적이지만 따뜻함은 잃지 않음. 이모지는 자제하되 🔥·📊·🎯 같은 핵심 강조용은 OK.',
},
instagram: {
id: 'instagram',
name: '아라',
role: 'Instagram 콘텐츠 리드',
emoji: '📷',
color: '#E1306C',
specialty: '인스타그램 릴스·피드 콘셉트, 캡션·해시태그 전략, 게시 시간 최적화, 스토리·하이라이트 기획, 팔로워 인게이지먼트 분석',
tagline: '인스타 콘텐츠와 인게이지먼트를 끌어올립니다',
roleCategory: 'planner',
persona: '시각·트렌드 감각이 빠른 콘텐츠 리드. "이 콘셉트는 지금 통합니다·아닙니다"를 짧고 분명하게 말함. 캡션은 후크 → 가치 → CTA 흐름. 이모지 적당히 (📷·✨·💬).',
},
};
/** Display order for the manage panel. CEO first, then specialists. */
/** 매니지 패널 표시 순서 — CEO가 맨 앞, 그 뒤로 제품 개발 파이프라인 순서. */
export const COMPANY_AGENT_ORDER: string[] = [
'ceo', 'youtube', 'instagram', 'designer', 'developer',
'business', 'secretary', 'editor', 'writer', 'researcher',
'qa', 'inspector',
'ceo',
'business', // PM — 기획
'researcher', // UX 리서치 — 검증
'designer', // 디자이너 — 화면 설계
'developer', // 엔지니어 — 구현
'qa', // QA — 검증
'inspector', // 감리·회고
'secretary', // 오퍼레이션
'writer', // 라이팅
'editor', // 사운드 디렉터 (선택 직군)
'youtube', // 마케팅 채널
'instagram', // 마케팅 채널
];
/** Specialists only (everything except the CEO). */
export const COMPANY_SPECIALIST_IDS: string[] = COMPANY_AGENT_ORDER.filter((id) => id !== 'ceo');
/** Default activation set used when a user first opens the company panel.
* 포함 기준: 13단계 풀 프로덕트 파이프라인을 그대로 돌릴 수 있는 최소 직군
* 세트. business는 inspector 직군이지만 너무 많이 끼면 작은 모델이
* 헷갈리므로 빼고, 진짜 감리 역할인 inspector(민지)를 넣는다. */
/**
* 처음 1인 기업 모드를 열 때 켜져 있는 디폴트 에이전트.
*
* 기준: 제품 개발 한 사이클(기획 → 디자인 → 개발 → QA → 감리)을 그대로 돌릴 수 있는
* 최소 코어. 콘텐츠/마케팅 직군(writer / youtube / instagram / designer_sound)은
* 사용자가 필요할 때 직접 켜도록 디폴트는 OFF — 작은 모델이 너무 많은 에이전트에
* 한꺼번에 노출되면 매칭이 흔들리기 때문.
*/
export const DEFAULT_ACTIVE_AGENTS: string[] = [
'ceo', 'writer', 'researcher', 'designer', 'developer', 'qa', 'inspector',
'ceo', 'business', 'researcher', 'designer', 'developer', 'qa', 'inspector',
];
/** Lookup helper. Returns `undefined` for unknown ids instead of throwing. */
+86 -5
View File
@@ -94,6 +94,7 @@ function _defaultState(): CompanyState {
displayOverrides: {},
pipelines: {},
activePipelineId: null,
hiddenBuiltinIds: [],
};
}
@@ -279,6 +280,13 @@ function _normalize(raw: Partial<CompanyState> | undefined): CompanyState {
&& Object.prototype.hasOwnProperty.call(pipelines, raw.activePipelineId)
? raw.activePipelineId
: null;
// 사용자가 "삭제"한 빌트인 에이전트 목록. CEO는 절대 hide할 수 없고, 모르는 id는
// drop (예: 코드에서 사라진 옛 에이전트). pipelines 가드는 remove 시점에 적용되므로
// 여기서는 별도 정합성 검사를 하지 않음.
const hiddenBuiltinIds = Array.isArray(raw.hiddenBuiltinIds)
? raw.hiddenBuiltinIds.filter((id): id is string =>
typeof id === 'string' && id !== 'ceo' && !!getCompanyAgent(id))
: [];
return {
enabled, companyName,
activeAgentIds: withoutCeo,
@@ -290,6 +298,7 @@ function _normalize(raw: Partial<CompanyState> | undefined): CompanyState {
displayOverrides,
pipelines,
activePipelineId,
hiddenBuiltinIds,
};
}
@@ -466,25 +475,97 @@ export async function removeCustomAgent(
context: vscode.ExtensionContext,
agentId: string,
): Promise<{ ok: true; state: CompanyState } | { ok: false; reason: string }> {
if (COMPANY_AGENTS[agentId]) {
return { ok: false, reason: '기본 에이전트는 삭제할 수 없습니다.' };
return removeCompanyAgent(context, agentId);
}
/**
* Delete *any* agent (built-in or custom). Built-ins can't actually be removed
* from the code, so we add them to `hiddenBuiltinIds` — the manage panel and
* dispatcher filters both honour that list. Custom agents are removed outright
* along with their overrides.
*
* Guards:
* - CEO은 절대 삭제 불가 — 회사 의사결정자 직군이라 항상 필요.
* - 어떤 파이프라인 stage라도 이 에이전트를 참조하면 거부. 사용자가 먼저
* 파이프라인에서 빼거나 다른 에이전트로 교체해야 함. 메시지에 사용 중인
* 파이프라인 이름을 그대로 적어 돌려준다 (사용자가 어디로 가서 고쳐야
* 하는지 바로 보이도록).
*/
export async function removeCompanyAgent(
context: vscode.ExtensionContext,
agentId: string,
): Promise<{ ok: true; state: CompanyState; kind: 'custom-removed' | 'builtin-hidden' } | { ok: false; reason: string }> {
if (!agentId) {
return { ok: false, reason: '에이전트 ID가 비어 있습니다.' };
}
if (agentId === 'ceo') {
return { ok: false, reason: 'CEO는 회사 의사결정자 직군이라 삭제할 수 없습니다.' };
}
const cur = readCompanyState(context);
if (!cur.customAgents || !cur.customAgents[agentId]) {
const isBuiltin = !!COMPANY_AGENTS[agentId];
const isCustom = !!cur.customAgents?.[agentId];
if (!isBuiltin && !isCustom) {
return { ok: false, reason: `'${agentId}' 에이전트를 찾을 수 없습니다.` };
}
const { [agentId]: _gone, ...customAgents } = cur.customAgents;
// 파이프라인 사용 검사 — 어디든 한 곳이라도 stage.agentId === agentId면 거부.
const usedIn: string[] = [];
for (const p of Object.values(cur.pipelines ?? {})) {
if (p.stages.some((s) => s.agentId === agentId)) {
usedIn.push(p.name || p.id);
}
}
if (usedIn.length > 0) {
const list = usedIn.map((n) => `'${n}'`).join(', ');
return {
ok: false,
reason: `다음 워크 파이프라인에서 사용 중이라 삭제할 수 없습니다: ${list}. 먼저 파이프라인에서 이 에이전트를 빼거나 다른 에이전트로 교체한 뒤 다시 시도하세요.`,
};
}
// 공통 override cleanup — 두 경로 모두 동일.
const { [agentId]: _m, ...modelOverrides } = cur.modelOverrides;
const { [agentId]: _p, ...promptOverrides } = cur.promptOverrides;
const { [agentId]: _k, ...knowledgeMixOverrides } = cur.knowledgeMixOverrides;
const { [agentId]: _r, ...roleCategoryOverrides } = (cur.roleCategoryOverrides ?? {});
const { [agentId]: _d, ...displayOverrides } = (cur.displayOverrides ?? {});
const activeAgentIds = cur.activeAgentIds.filter((id) => id !== agentId);
if (isCustom) {
const { [agentId]: _gone, ...customAgents } = (cur.customAgents ?? {});
const next: CompanyState = {
...cur, customAgents, modelOverrides, promptOverrides, knowledgeMixOverrides,
roleCategoryOverrides, displayOverrides, activeAgentIds,
};
await writeCompanyState(context, next);
return { ok: true, state: next, kind: 'custom-removed' };
}
// Built-in: add to hiddenBuiltinIds (idempotent) and drop overrides.
const hidden = new Set(cur.hiddenBuiltinIds ?? []);
hidden.add(agentId);
const next: CompanyState = {
...cur, customAgents, modelOverrides, promptOverrides, knowledgeMixOverrides,
...cur, modelOverrides, promptOverrides, knowledgeMixOverrides,
roleCategoryOverrides, displayOverrides, activeAgentIds,
hiddenBuiltinIds: Array.from(hidden),
};
await writeCompanyState(context, next);
return { ok: true, state: next, kind: 'builtin-hidden' };
}
/**
* Restore a previously-hidden built-in agent back into the manage panel.
* No-op if the id wasn't hidden. Custom agents have no "restore" — once
* deleted they're gone and must be re-created via the add form.
*/
export async function restoreHiddenAgent(
context: vscode.ExtensionContext,
agentId: string,
): Promise<{ ok: true; state: CompanyState } | { ok: false; reason: string }> {
if (!agentId || !COMPANY_AGENTS[agentId]) {
return { ok: false, reason: `'${agentId}'은(는) 빌트인 에이전트가 아닙니다.` };
}
const cur = readCompanyState(context);
const hidden = (cur.hiddenBuiltinIds ?? []).filter((id) => id !== agentId);
const next: CompanyState = { ...cur, hiddenBuiltinIds: hidden };
await writeCompanyState(context, next);
return { ok: true, state: next };
}
+2
View File
@@ -24,6 +24,8 @@ export {
setAgentKnowledgeMix,
addCustomAgent,
removeCustomAgent,
removeCompanyAgent,
restoreHiddenAgent,
validateCustomAgentId,
setAgentRoleCategory,
setAgentDisplayOverride,
+10
View File
@@ -194,6 +194,16 @@ export interface CompanyState {
* planner path. Must reference a key in `pipelines` or it is ignored.
*/
activePipelineId?: string | null;
/**
* Built-in agent ids that the user has chosen to hide from the manage
* panel. We can't truly delete a code-defined agent, but we can suppress
* it from the UI / activation lists so the user perceives the same
* "삭제" outcome. The `restoreHiddenAgent` path can put it back.
*
* Pipeline guards still apply at delete time — an agent referenced by
* any pipeline stage cannot be moved into this list.
*/
hiddenBuiltinIds?: string[];
}
/**
+22 -3
View File
@@ -299,13 +299,32 @@ export async function handleChatMessage(provider: SidebarChatProvider, data: any
return true;
}
case 'deleteCompanyAgent': {
// Drop a user-defined agent. Built-ins refuse — backend enforces.
const { removeCustomAgent } = await import('../features/company');
// Delete any agent (built-in via hide, custom via outright removal).
// Backend checks pipeline usage and refuses if any stage references it.
const { removeCompanyAgent } = await import('../features/company');
const agentId = typeof data.agentId === 'string' ? data.agentId : '';
if (!agentId) return true;
const result = await removeCustomAgent(provider._context, agentId);
const result = await removeCompanyAgent(provider._context, agentId);
provider._view?.webview.postMessage({
type: 'deleteCompanyAgentResult',
value: result.ok
? { ok: true, agentId, kind: result.kind }
: { ok: false, agentId, reason: result.reason },
});
if (result.ok) {
await provider._sendCompanyStatus();
await provider._sendCompanyAgents();
}
return true;
}
case 'restoreHiddenAgent': {
// Bring a previously-hidden built-in back into the manage panel.
const { restoreHiddenAgent } = await import('../features/company');
const agentId = typeof data.agentId === 'string' ? data.agentId : '';
if (!agentId) return true;
const result = await restoreHiddenAgent(provider._context, agentId);
provider._view?.webview.postMessage({
type: 'restoreHiddenAgentResult',
value: result.ok ? { ok: true, agentId } : { ok: false, reason: result.reason },
});
if (result.ok) {
+24 -2
View File
@@ -1582,10 +1582,20 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
const globalWeight = cfg.knowledgeMixSecondBrainWeight ?? 50;
// Built-ins first (insertion order from agents.ts), then user-added
// customs in their own order. `custom: true` lets the UI render a
// delete button only for user-added entries.
const builtinIds = COMPANY_AGENT_ORDER.filter((id) => !!COMPANY_AGENTS[id]);
// delete button differently for user-added entries.
const hiddenSet = new Set(state.hiddenBuiltinIds ?? []);
const builtinIds = COMPANY_AGENT_ORDER.filter((id) => !!COMPANY_AGENTS[id] && !hiddenSet.has(id));
const customIds = state.customAgents ? Object.keys(state.customAgents) : [];
const orderedIds = [...builtinIds, ...customIds];
// 파이프라인에서 사용 중인 에이전트 id별로 사용처(파이프라인 이름) 목록을 미리 계산.
// 카드의 삭제 버튼 비활성화 + tooltip에 사용 중인 파이프라인 이름을 노출하는 데 사용.
const pipelineUsage: Record<string, string[]> = {};
for (const p of Object.values(state.pipelines ?? {})) {
for (const s of p.stages) {
if (!s.agentId) continue;
(pipelineUsage[s.agentId] = pipelineUsage[s.agentId] ?? []).push(p.name || p.id);
}
}
const renderEntry = (id: string) => {
const builtin = COMPANY_AGENTS[id];
const custom = state.customAgents?.[id];
@@ -1635,15 +1645,27 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
roleCategoryOverridden: !!roleOverride && roleOverride !== baseDef.roleCategory,
knowledgeMixOverride: hasKmOverride ? kmOverride : null,
effectiveKnowledgeMixWeight: hasKmOverride ? kmOverride : globalWeight,
// 파이프라인 사용 현황. 비어 있으면 삭제 가능, 한 개라도 있으면 UI에서
// 삭제 버튼을 disabled로 처리하고 tooltip에 사용 중인 파이프라인을 노출.
usedInPipelines: pipelineUsage[id] ? [...new Set(pipelineUsage[id])] : [],
};
};
const agents = orderedIds.map(renderEntry).filter((x): x is NonNullable<ReturnType<typeof renderEntry>> => !!x);
// 숨김 처리된 빌트인 — 사용자에게 "복원" 옵션을 제공하기 위해 한 줄 메타로 같이 보낸다.
const hiddenBuiltins = (state.hiddenBuiltinIds ?? [])
.map((id) => {
const def = COMPANY_AGENTS[id];
if (!def) return null;
return { id, name: def.name, role: def.role, emoji: def.emoji };
})
.filter((x): x is { id: string; name: string; role: string; emoji: string } => !!x);
this._view.webview.postMessage({
type: 'companyAgents',
value: {
companyName: state.companyName,
globalKnowledgeMixWeight: globalWeight,
agents,
hiddenBuiltins,
// 직군 라벨 사전 + 표시 순서. 웹뷰는 enum 값을 모르므로
// 백엔드가 정한 라벨/순서를 같이 보내 UI 일관성을 유지.
roleCategoryLabels: ROLE_CATEGORY_LABELS,