refactor: Fine-tune sidebar interaction and refine company suite configuration

This commit is contained in:
2026-05-14 18:04:25 +09:00
parent 75d7e6b83a
commit d84e02c696
13 changed files with 215 additions and 33 deletions
+88 -7
View File
@@ -21,8 +21,8 @@
import * as vscode from 'vscode';
import { COMPANY_AGENTS, DEFAULT_ACTIVE_AGENTS, getCompanyAgent } from './agents';
import {
AgentPromptOverride, AgentRoleCategory, CompanyAgentDef, CompanyState, COMPANY_STATE_KEY,
PipelineDef, PipelineStage, ROLE_CATEGORY_ORDER,
AgentDisplayOverride, AgentPromptOverride, AgentRoleCategory, CompanyAgentDef,
CompanyState, COMPANY_STATE_KEY, PipelineDef, PipelineStage, ROLE_CATEGORY_ORDER,
} from './types';
const VALID_ROLE_CATEGORIES = new Set<AgentRoleCategory>(ROLE_CATEGORY_ORDER);
@@ -91,6 +91,7 @@ function _defaultState(): CompanyState {
knowledgeMixOverrides: {},
customAgents: {},
roleCategoryOverrides: {},
displayOverrides: {},
pipelines: {},
activePipelineId: null,
};
@@ -244,6 +245,26 @@ function _normalize(raw: Partial<CompanyState> | undefined): CompanyState {
roleCategoryOverrides[agentId] = v as AgentRoleCategory;
}
}
// 디스플레이 override — 이름·역할·이모지·색상 사용자 편집. 빈 필드는
// pruning, 알 수 없는 agent id는 drop.
const displayOverrides: Record<string, AgentDisplayOverride> = {};
if (raw.displayOverrides && typeof raw.displayOverrides === 'object') {
for (const [agentId, v] of Object.entries(raw.displayOverrides as Record<string, unknown>)) {
if (!knownId(agentId) || !v || typeof v !== 'object') continue;
const ov = v as Record<string, unknown>;
const cleaned: AgentDisplayOverride = {};
if (typeof ov.name === 'string' && ov.name.trim()) cleaned.name = ov.name.trim();
if (typeof ov.role === 'string' && ov.role.trim()) cleaned.role = ov.role.trim();
if (typeof ov.emoji === 'string' && ov.emoji.trim()) cleaned.emoji = ov.emoji.trim();
if (typeof ov.color === 'string' && /^#?[0-9a-fA-F]{3,8}$/.test(ov.color.trim())) {
const c = ov.color.trim();
cleaned.color = c.startsWith('#') ? c : '#' + c;
}
if (cleaned.name || cleaned.role || cleaned.emoji || cleaned.color) {
displayOverrides[agentId] = cleaned;
}
}
}
// Pipelines — drop malformed entries; stage agent ids that don't resolve
// are kept (the dispatcher will surface a per-stage error) so the user
// can fix them in the editor instead of losing their pipeline silently.
@@ -266,6 +287,7 @@ function _normalize(raw: Partial<CompanyState> | undefined): CompanyState {
knowledgeMixOverrides,
customAgents,
roleCategoryOverrides,
displayOverrides,
pipelines,
activePipelineId,
};
@@ -456,15 +478,64 @@ export async function removeCustomAgent(
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);
const next: CompanyState = {
...cur, customAgents, modelOverrides, promptOverrides, knowledgeMixOverrides,
roleCategoryOverrides, activeAgentIds,
roleCategoryOverrides, displayOverrides, activeAgentIds,
};
await writeCompanyState(context, next);
return { ok: true, state: next };
}
/**
* Set / clear an agent's display-identity override (name / role / emoji /
* color). Each field follows the same semantics as `setAgentPromptOverride`:
* - non-empty string → save as override
* - empty string / null / undefined → *clear* that sub-field
* Passing `null` for the whole override clears every field at once.
* Built-in CEO can be renamed too — there's no reason to lock the chip text.
*/
export async function setAgentDisplayOverride(
context: vscode.ExtensionContext,
agentId: string,
override: AgentDisplayOverride | null,
): Promise<{ ok: true; state: CompanyState } | { ok: false; reason: string }> {
const cur = readCompanyState(context);
if (!getCompanyAgent(agentId) && !cur.customAgents?.[agentId]) {
return { ok: false, reason: `'${agentId}' 에이전트를 찾을 수 없습니다.` };
}
const overrides = { ...(cur.displayOverrides ?? {}) };
if (!override) {
delete overrides[agentId];
} else {
const existing: AgentDisplayOverride = { ...(overrides[agentId] ?? {}) };
for (const key of ['name', 'role', 'emoji', 'color'] as const) {
const v = override[key];
if (v === undefined) continue;
if (typeof v === 'string' && v.trim()) {
if (key === 'color') {
if (!/^#?[0-9a-fA-F]{3,8}$/.test(v.trim())) continue;
const c = v.trim();
existing[key] = c.startsWith('#') ? c : '#' + c;
} else {
existing[key] = v.trim();
}
} else {
delete existing[key];
}
}
if (existing.name || existing.role || existing.emoji || existing.color) {
overrides[agentId] = existing;
} else {
delete overrides[agentId];
}
}
const next: CompanyState = { ...cur, displayOverrides: overrides };
await writeCompanyState(context, next);
return { ok: true, state: next };
}
/**
* Set / clear a per-agent 직군 override. Pass `null` to revert the agent
* to its def's own roleCategory. The CEO can't be reclassified (always 'ceo').
@@ -569,11 +640,21 @@ export function resolveActivePipeline(state: CompanyState): PipelineDef | null {
export function resolveAgent(state: CompanyState, agentId: string): CompanyAgentDef | undefined {
const base = getCompanyAgent(agentId) ?? state.customAgents?.[agentId];
if (!base) return undefined;
const ov = state.roleCategoryOverrides?.[agentId];
if (ov && VALID_ROLE_CATEGORIES.has(ov) && ov !== base.roleCategory && agentId !== 'ceo') {
return { ...base, roleCategory: ov };
const display = state.displayOverrides?.[agentId];
const roleOv = state.roleCategoryOverrides?.[agentId];
// 머지 필요 없으면 base를 그대로 반환 — 불필요한 객체 복제 방지.
const hasDisplay = !!(display && (display.name || display.role || display.emoji || display.color));
const hasRole = !!(roleOv && VALID_ROLE_CATEGORIES.has(roleOv) && roleOv !== base.roleCategory && agentId !== 'ceo');
if (!hasDisplay && !hasRole) return base;
const merged: CompanyAgentDef = { ...base };
if (hasDisplay) {
if (display!.name) merged.name = display!.name;
if (display!.role) merged.role = display!.role;
if (display!.emoji) merged.emoji = display!.emoji;
if (display!.color) merged.color = display!.color;
}
return base;
if (hasRole) merged.roleCategory = roleOv!;
return merged;
}
/**
+2
View File
@@ -26,6 +26,7 @@ export {
removeCustomAgent,
validateCustomAgentId,
setAgentRoleCategory,
setAgentDisplayOverride,
upsertPipeline,
deletePipeline,
setActivePipeline,
@@ -43,6 +44,7 @@ export {
} from './companyConfig';
export type {
AgentDisplayOverride,
AgentRoleCategory,
PipelineDef,
PipelineStage,
+28
View File
@@ -102,6 +102,27 @@ export interface AgentPromptOverride {
tagline?: string;
}
/**
* User edits to an agent's *identity* surface (name / role title / emoji /
* color). Unlike prompt overrides these don't affect the LLM's behaviour
* directly — they change how the agent is *referred to* across the UI,
* planner enumeration, and CEO report. Built-ins ship with fixed defaults;
* the user can rename "레오" to "Lily" via this override without forking
* the shipped roster. CEO planner's `_canonicalAgentId` already matches
* against the effective (override-applied) name, so renaming an agent
* still lets the LLM dispatch to it correctly.
*/
export interface AgentDisplayOverride {
/** Replaces the display name (e.g. "레오" → "Lily"). */
name?: string;
/** Replaces the role title (e.g. "Head of YouTube" → "콘텐츠 디렉터"). */
role?: string;
/** Replaces the single emoji icon. */
emoji?: string;
/** Replaces the brand color (CSS hex). */
color?: string;
}
/**
* Persisted runtime state for the company mode. Stored in VS Code's
* `globalState` plus a small JSON file under `.astra/company/_shared/`.
@@ -154,6 +175,13 @@ export interface CompanyState {
* agent def's own `roleCategory`.
*/
roleCategoryOverrides?: Record<string, AgentRoleCategory>;
/**
* Per-agent identity overrides (name / role / emoji / color). Same
* semantics as `promptOverrides` but for the display surface — the
* effective values are merged into the def at read time via
* `resolveAgent`.
*/
displayOverrides?: Record<string, AgentDisplayOverride>;
/**
* User-defined work pipelines. When `activePipelineId` is set to a key
* here, the dispatcher runs the pipeline's stages in order instead of
+23
View File
@@ -206,6 +206,29 @@ export async function handleChatMessage(provider: SidebarChatProvider, data: any
}
return true;
}
case 'setCompanyAgentDisplay': {
// 이름/역할/이모지/색상 override. 페이로드는 setCompanyAgentPrompt와
// 동일한 패턴 — null이면 전체 reset, 각 필드 빈 문자열이면 그 필드만 reset.
const { setAgentDisplayOverride } = 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
: {
name: typeof v?.name === 'string' ? v.name : undefined,
role: typeof v?.role === 'string' ? v.role : undefined,
emoji: typeof v?.emoji === 'string' ? v.emoji : undefined,
color: typeof v?.color === 'string' ? v.color : undefined,
};
const result = await setAgentDisplayOverride(provider._context, agentId, override);
provider._view?.webview.postMessage({
type: 'setCompanyAgentDisplayResult',
value: result.ok ? { ok: true, agentId } : { ok: false, reason: result.reason },
});
if (result.ok) await provider._sendCompanyAgents();
return true;
}
case 'setCompanyAgentRoleCategory': {
// Override an agent's 직군. Empty / null payload value reverts to
// the def's own roleCategory. CEO is rejected by the backend.
+11
View File
@@ -1599,6 +1599,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
const kmOverride = state.knowledgeMixOverrides[id];
const hasKmOverride = typeof kmOverride === 'number';
const roleOverride = state.roleCategoryOverrides?.[id];
const displayOv = state.displayOverrides?.[id] || {};
return {
id,
name: effective.name,
@@ -1618,6 +1619,16 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
personaOverridden: !!override.persona,
specialtyOverridden: !!override.specialty,
taglineOverridden: !!override.tagline,
// 디스플레이 override — Edit 폼이 default vs current를 비교해
// dirty 표시할 수 있도록 baseDef의 원본 값도 같이 보낸다.
defaultName: baseDef.name,
defaultRole: baseDef.role,
defaultEmoji: baseDef.emoji,
defaultColor: baseDef.color,
nameOverridden: !!displayOv.name,
roleOverridden: !!displayOv.role,
emojiOverridden: !!displayOv.emoji,
colorOverridden: !!displayOv.color,
// 직군: effective(override 반영) + def 기본값 + override 플래그
roleCategory: effective.roleCategory,
defaultRoleCategory: baseDef.roleCategory,