feat: Stabilize Company Suite & Self-Reflection logic, integrate new ADRs and bug records
This commit is contained in:
+99
-19
@@ -42,6 +42,9 @@ import {
|
||||
CompanyTurnEvent,
|
||||
COMPANY_AGENTS,
|
||||
COMPANY_AGENT_ORDER,
|
||||
ROLE_CATEGORY_LABELS,
|
||||
ROLE_CATEGORY_ORDER,
|
||||
resolveAgent,
|
||||
} from './features/company';
|
||||
import { AIService } from './core/services';
|
||||
|
||||
@@ -91,6 +94,14 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
_modelsCache: { url: string; models: string[]; online: boolean; fetchedAt: number } | null = null;
|
||||
static readonly MODELS_CACHE_TTL_MS = 30000;
|
||||
|
||||
/**
|
||||
* AbortController for the currently-running 1인 기업 turn. Cleared when
|
||||
* the turn ends (success or fail). The webview's Stop button routes
|
||||
* through `stopGeneration`, which calls `abortCompanyTurn()` to flip this
|
||||
* — the dispatcher's `signal` then short-circuits between phases.
|
||||
*/
|
||||
private _companyAbort?: AbortController;
|
||||
|
||||
/** Active file-system watcher for the project-architecture auto-update. Disposed on switch/detach. */
|
||||
private _archWatcher?: vscode.FileSystemWatcher;
|
||||
/** Debounce timer for the architecture watcher. */
|
||||
@@ -1453,6 +1464,49 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
return readCompanyState(this._context).enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort the currently-running 1인 기업 turn if any. Returns true when an
|
||||
* abort was actually fired (so the chat handler can skip `agent.stop()`
|
||||
* — the company path never touches AgentExecutor). The dispatcher will
|
||||
* see `signal.aborted` at its next phase boundary and emit
|
||||
* `phase: 'aborted'`; `_runCompanyTurn`'s finally clause then posts
|
||||
* `streamEnd` so the UI unlocks.
|
||||
*/
|
||||
abortCompanyTurn(): boolean {
|
||||
if (!this._companyAbort) return false;
|
||||
this._companyAbort.abort();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push the full pipeline catalogue + active id to the webview so the
|
||||
* editor overlay can render the cards. Pipelines are user-defined
|
||||
* (no built-ins) so an empty list is the default for new users.
|
||||
*/
|
||||
async _sendCompanyPipelines(): Promise<void> {
|
||||
if (!this._view) return;
|
||||
const state = readCompanyState(this._context);
|
||||
// 직군별 활성 에이전트도 같이 — 파이프라인 에디터가 "직군 → 담당자"
|
||||
// cascading dropdown을 채울 때 이 페이로드만 보고 그릴 수 있게.
|
||||
const { listActiveAgentsByCategory } = await import('./features/company');
|
||||
const byCategory = listActiveAgentsByCategory(state);
|
||||
// CompanyAgentDef를 통째로 보내는 대신 UI에 필요한 필드만 추려서.
|
||||
const slimByCategory: Record<string, Array<{ id: string; name: string; emoji: string }>> = {};
|
||||
for (const [cat, defs] of Object.entries(byCategory)) {
|
||||
slimByCategory[cat] = defs.map((d) => ({ id: d.id, name: d.name, emoji: d.emoji }));
|
||||
}
|
||||
this._view.webview.postMessage({
|
||||
type: 'companyPipelines',
|
||||
value: {
|
||||
pipelines: state.pipelines ?? {},
|
||||
activePipelineId: state.activePipelineId ?? null,
|
||||
roleCategoryLabels: ROLE_CATEGORY_LABELS,
|
||||
roleCategoryOrder: ROLE_CATEGORY_ORDER,
|
||||
activeAgentsByCategory: slimByCategory,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Send the chip state (active flag + agent count + name) to the webview. */
|
||||
async _sendCompanyStatus(): Promise<void> {
|
||||
if (!this._view) return;
|
||||
@@ -1480,44 +1534,63 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
const state = readCompanyState(this._context);
|
||||
const cfg = getConfig();
|
||||
const globalWeight = cfg.knowledgeMixSecondBrainWeight ?? 50;
|
||||
const agents = COMPANY_AGENT_ORDER.map((id) => {
|
||||
const def = COMPANY_AGENTS[id];
|
||||
// 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]);
|
||||
const customIds = state.customAgents ? Object.keys(state.customAgents) : [];
|
||||
const orderedIds = [...builtinIds, ...customIds];
|
||||
const renderEntry = (id: string) => {
|
||||
const builtin = COMPANY_AGENTS[id];
|
||||
const custom = state.customAgents?.[id];
|
||||
const baseDef = builtin ?? custom;
|
||||
if (!baseDef) return null;
|
||||
// 직군 override 적용된 effective def. 카드의 드롭다운이 옳은 선택값을
|
||||
// 보이려면 override 결과를 보내야 한다.
|
||||
const effective = resolveAgent(state, id) ?? baseDef;
|
||||
const isCustom = !builtin;
|
||||
const override = state.promptOverrides[id] || {};
|
||||
const kmOverride = state.knowledgeMixOverrides[id];
|
||||
const hasKmOverride = typeof kmOverride === 'number';
|
||||
const roleOverride = state.roleCategoryOverrides?.[id];
|
||||
return {
|
||||
id,
|
||||
name: def.name,
|
||||
role: def.role,
|
||||
emoji: def.emoji,
|
||||
color: def.color,
|
||||
alwaysOn: !!def.alwaysOn,
|
||||
name: effective.name,
|
||||
role: effective.role,
|
||||
emoji: effective.emoji,
|
||||
color: effective.color,
|
||||
alwaysOn: !!effective.alwaysOn,
|
||||
custom: isCustom,
|
||||
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.
|
||||
defaultTagline: baseDef.tagline,
|
||||
defaultSpecialty: baseDef.specialty,
|
||||
defaultPersona: baseDef.persona || '',
|
||||
tagline: override.tagline || baseDef.tagline,
|
||||
specialty: override.specialty || baseDef.specialty,
|
||||
persona: override.persona || baseDef.persona || '',
|
||||
personaOverridden: !!override.persona,
|
||||
specialtyOverridden: !!override.specialty,
|
||||
taglineOverridden: !!override.tagline,
|
||||
// Knowledge Mix — null when using global default, number otherwise.
|
||||
// 직군: effective(override 반영) + def 기본값 + override 플래그
|
||||
roleCategory: effective.roleCategory,
|
||||
defaultRoleCategory: baseDef.roleCategory,
|
||||
roleCategoryOverridden: !!roleOverride && roleOverride !== baseDef.roleCategory,
|
||||
knowledgeMixOverride: hasKmOverride ? kmOverride : null,
|
||||
// What the dispatcher *will actually use* this turn (for hint UI).
|
||||
effectiveKnowledgeMixWeight: hasKmOverride ? kmOverride : globalWeight,
|
||||
};
|
||||
});
|
||||
};
|
||||
const agents = orderedIds.map(renderEntry).filter((x): x is NonNullable<ReturnType<typeof renderEntry>> => !!x);
|
||||
this._view.webview.postMessage({
|
||||
type: 'companyAgents',
|
||||
value: {
|
||||
companyName: state.companyName,
|
||||
globalKnowledgeMixWeight: globalWeight,
|
||||
agents,
|
||||
// 직군 라벨 사전 + 표시 순서. 웹뷰는 enum 값을 모르므로
|
||||
// 백엔드가 정한 라벨/순서를 같이 보내 UI 일관성을 유지.
|
||||
roleCategoryLabels: ROLE_CATEGORY_LABELS,
|
||||
roleCategoryOrder: ROLE_CATEGORY_ORDER,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1534,6 +1607,11 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
const emit = (event: CompanyTurnEvent) => {
|
||||
this._view?.webview.postMessage({ type: 'companyTurnUpdate', value: event });
|
||||
};
|
||||
// Fresh AbortController per turn — the Stop button routes through
|
||||
// `abortCompanyTurn()` to fire `.abort()`. The dispatcher checks
|
||||
// `signal.aborted` between phases and short-circuits cleanly.
|
||||
const abort = new AbortController();
|
||||
this._companyAbort = abort;
|
||||
try {
|
||||
await runCompanyTurn(userPrompt, {
|
||||
context: this._context,
|
||||
@@ -1550,6 +1628,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
// hit disk. Without this, agents would *claim* to create
|
||||
// files while nothing happened — the exact bug we just fixed.
|
||||
executeActionTags: (text) => this._agent.executeActionTagsOnText(text),
|
||||
signal: abort.signal,
|
||||
onEvent: emit,
|
||||
});
|
||||
} catch (e: any) {
|
||||
@@ -1559,6 +1638,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
value: `1인 기업 모드 실행 실패: ${e?.message ?? e}`,
|
||||
});
|
||||
} finally {
|
||||
if (this._companyAbort === abort) this._companyAbort = undefined;
|
||||
// The webview's send button is locked into the "generating" state
|
||||
// when the user submits; it only unlocks on `streamEnd`. The
|
||||
// normal chat path posts that from inside AgentExecutor, but
|
||||
|
||||
Reference in New Issue
Block a user