feat: Implement Pipeline Templates for Company Suite and refine orchestration logic

This commit is contained in:
2026-05-14 17:36:15 +09:00
parent 618b8d5b34
commit 75d7e6b83a
19 changed files with 1181 additions and 50 deletions
+57
View File
@@ -45,6 +45,7 @@ import {
ROLE_CATEGORY_LABELS,
ROLE_CATEGORY_ORDER,
resolveAgent,
PIPELINE_TEMPLATES,
} from './features/company';
import { AIService } from './core/services';
@@ -102,6 +103,21 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
*/
private _companyAbort?: AbortController;
/**
* Open approval gates. The dispatcher emits `phase: 'awaiting-approval'`
* for stages with `requiresApproval`, and waits on a Promise this map
* stores. The webview surfaces 승인 / 수정요청 / 중단 buttons; clicks
* route through `chatHandlers.respondCompanyApproval` which calls
* `resolveApprovalGate(stageId, decision)` here.
*
* Keyed by stageId — only one approval may be pending per stage at a
* time (sequential dispatch), but multiple stages across the same turn
* each get their own entry as they hit their gate. On turn abort we
* resolve all outstanding entries with `{ kind: 'abort' }` so the
* dispatcher unblocks cleanly.
*/
private _pendingApprovals = new Map<string, (d: import('./features/company/dispatcher').ApprovalDecision) => void>();
/** Active file-system watcher for the project-architecture auto-update. Disposed on switch/detach. */
private _archWatcher?: vscode.FileSystemWatcher;
/** Debounce timer for the architecture watcher. */
@@ -1475,6 +1491,25 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
abortCompanyTurn(): boolean {
if (!this._companyAbort) return false;
this._companyAbort.abort();
// 승인 게이트 대기 중인 모든 stage를 'abort'로 해제. 안 하면 dispatcher가
// 영원히 await 상태로 남아 turn이 절대 종료 안 됨.
for (const resolve of this._pendingApprovals.values()) {
try { resolve({ kind: 'abort' }); } catch { /* noop */ }
}
this._pendingApprovals.clear();
return true;
}
/**
* Called by chatHandlers when the user clicks an approval card button.
* Resolves the dispatcher's awaitApproval promise for `stageId`. Idempotent
* — extra clicks after the first one are silently dropped.
*/
resolveApprovalGate(stageId: string, decision: import('./features/company/dispatcher').ApprovalDecision): boolean {
const resolve = this._pendingApprovals.get(stageId);
if (!resolve) return false;
this._pendingApprovals.delete(stageId);
try { resolve(decision); } catch { /* noop */ }
return true;
}
@@ -1495,6 +1530,16 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
for (const [cat, defs] of Object.entries(byCategory)) {
slimByCategory[cat] = defs.map((d) => ({ id: d.id, name: d.name, emoji: d.emoji }));
}
// 템플릿 카탈로그 — 가벼운 메타데이터만 (stages는 stamp 시점에 한 번
// 더 요청). UI는 dropdown 옵션 텍스트만 필요.
const templates = PIPELINE_TEMPLATES.map((t) => ({
templateId: t.templateId,
name: t.name,
description: t.description,
stageCount: t.stages.length,
suggestedPipelineId: t.suggestedPipelineId,
suggestedPipelineName: t.suggestedPipelineName,
}));
this._view.webview.postMessage({
type: 'companyPipelines',
value: {
@@ -1503,6 +1548,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
roleCategoryLabels: ROLE_CATEGORY_LABELS,
roleCategoryOrder: ROLE_CATEGORY_ORDER,
activeAgentsByCategory: slimByCategory,
templates,
},
});
}
@@ -1630,6 +1676,17 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
executeActionTags: (text) => this._agent.executeActionTagsOnText(text),
signal: abort.signal,
onEvent: emit,
// 승인 게이트 bridge — dispatcher가 호출하면 Promise를 만들어
// resolver를 _pendingApprovals에 보관 후 await. 사용자가 카드 버튼을
// 누르면 chatHandlers가 resolveApprovalGate(stageId, decision)을 호출
// 하고 그 resolve가 이 await을 풀어준다.
awaitApproval: ({ stageId }) => new Promise((resolve) => {
if (abort.signal.aborted) {
resolve({ kind: 'abort' });
return;
}
this._pendingApprovals.set(stageId, resolve);
}),
});
} catch (e: any) {
logError('company.runTurn: unexpected failure.', { error: e?.message ?? String(e) });