287 lines
9.8 KiB
TypeScript
287 lines
9.8 KiB
TypeScript
/**
|
|
* Pixel Office — Agent Work Pipeline 상태를 시각화하는 *UI Layer 전용* 모듈.
|
|
*
|
|
* ─────────────────── 설계 원칙 ───────────────────
|
|
* 1. **Agent 핵심 판단 로직을 절대 바꾸지 않는다.** Pipeline 진행, contract
|
|
* 합의, 검수 cycle, 승인 게이트 — 모두 기존 dispatcher / chatHandlers /
|
|
* SidebarChatProvider 안에서 결정된다. 이 모듈은 그 결정을 *읽고* webview용
|
|
* 상태 객체로 변환할 뿐이다.
|
|
* 2. 입력은 기존에 emit되던 이벤트 (CompanyTurnEvent + alignment phase +
|
|
* intent classifier 결과)뿐. 새로운 인터럽트 포인트를 만들지 않는다.
|
|
* 3. 출력은 두 종류 — `AgentWorkState` (현재 작업 패널)와 `AgentBubble` 큐
|
|
* (말풍선 연출). 둘 다 webview가 그대로 받아 그리기만 하면 된다.
|
|
*
|
|
* 즉 dispatcher 안의 어떤 한 줄도 "if pixelOffice ..."로 분기하지 않는다.
|
|
*/
|
|
|
|
/** Agent의 현재 단계. 사용자가 명세한 11개 상태값 전부 포함. */
|
|
export type AgentStatus =
|
|
| 'idle'
|
|
| 'intake'
|
|
| 'analyzing'
|
|
| 'need_clarification'
|
|
| 'contract_ready'
|
|
| 'planning'
|
|
| 'executing'
|
|
| 'reviewing'
|
|
| 'waiting_approval'
|
|
| 'error'
|
|
| 'done';
|
|
|
|
/** 말풍선이 어떤 카테고리에서 나왔는지 — 스타일링 / 우선순위에 활용. */
|
|
export type BubbleType = 'status' | 'event' | 'warning' | 'error' | 'success';
|
|
|
|
/** 말풍선 발생 트리거가 되는 이벤트 — 사용자가 명세한 10개 + 약간 확장. */
|
|
export type AgentEvent =
|
|
| 'missing_required_info'
|
|
| 'clarification_needed'
|
|
| 'requirement_contract_created'
|
|
| 'plan_completed'
|
|
| 'execution_started'
|
|
| 'review_failed'
|
|
| 'review_passed'
|
|
| 'risky_change_detected'
|
|
| 'approval_required'
|
|
| 'error_occurred'
|
|
| 'task_completed'
|
|
| 'stage_loop_retry';
|
|
|
|
export interface AgentWorkState {
|
|
agentId: string;
|
|
agentName: string;
|
|
status: AgentStatus;
|
|
/** 사용자 원본 요청 한 줄 — 패널 상단 "Current Task"에 표시. */
|
|
currentTask?: string;
|
|
/** 현재 stage / phase 라벨 — "기획 논의", "QA 진행" 등. */
|
|
currentStep?: string;
|
|
/** 다음 stage 라벨 (있으면) — 예측 표시용. */
|
|
nextStep?: string;
|
|
/** 짧은 보조 메시지 (예: "라운드 2/3", "검수자: 민지"). */
|
|
message?: string;
|
|
/** 진행률 0~1 — 파이프라인 모드일 때 stage index / total로 계산. */
|
|
progress?: number;
|
|
/** Requirement Contract 요약 (alignment 완료 후 채워짐). */
|
|
requirementContract?: {
|
|
goal?: string;
|
|
context?: string;
|
|
criteria?: string[];
|
|
format?: string;
|
|
openQuestions?: string[];
|
|
confidence?: 'low' | 'medium' | 'high';
|
|
};
|
|
/** 사용자에게 던지는 미해결 질문 목록 — need_clarification 상태에서 채움. */
|
|
needUserInput?: string[];
|
|
/** 승인 대기 중 항목 — waiting_approval 상태에서 채움. */
|
|
awaitingApproval?: string;
|
|
/** 짧은 최근 로그 — 사용자가 한눈에 흐름 파악. 최대 6개 ring buffer. */
|
|
recentLogs?: string[];
|
|
/**
|
|
* 파이프라인 미니 맵용 stage 리스트. `plan-ready` 시점에 채워지고 agent-start/
|
|
* agent-done 으로 상태가 갱신된다. webview는 이 리스트를 dot strip 으로 렌더.
|
|
* - status: 'done' | 'active' | 'pending'
|
|
*/
|
|
pipelineStages?: Array<{ label: string; agent?: string; status: 'done' | 'active' | 'pending' }>;
|
|
/** epoch ms — webview의 "n초 전" 표시용. */
|
|
updatedAt: number;
|
|
}
|
|
|
|
export interface AgentBubble {
|
|
id: string;
|
|
/** 어떤 캐릭터 위에 띄울지 — 단일 캐릭터 모드면 'main' 고정도 가능. */
|
|
agentId: string;
|
|
text: string;
|
|
type: BubbleType;
|
|
/** 생성 시각 epoch ms. */
|
|
createdAt: number;
|
|
/** 자동 사라짐 ms (webview가 사용). 기본값은 webview에서 결정. */
|
|
durationMs?: number;
|
|
}
|
|
|
|
/**
|
|
* 사용자가 설정으로 켜고 끌 수 있는 행동 옵션.
|
|
* webview는 broadcast마다 같이 받아서 즉시 반영.
|
|
*/
|
|
export interface PixelOfficeConfig {
|
|
enabled: boolean;
|
|
bubblesEnabled: boolean;
|
|
maxVisibleBubbles: number;
|
|
bubbleDurationMs: number;
|
|
}
|
|
|
|
// ─────────────────── 상태→말풍선 텍스트 풀 ───────────────────
|
|
// 사용자가 명세한 톤(가벼운 사무실 코미디) 유지. 무작위 선택을 위해 같은 상태에
|
|
// 여러 안을 두되 너무 길어지지 않게 4~5개로 제한.
|
|
|
|
const STATUS_BUBBLE_POOL: Record<AgentStatus, string[]> = {
|
|
idle: [
|
|
'오늘은 무슨 일을 할까?',
|
|
'주문 대기 중…',
|
|
'커피 한 잔 더 하고 시작할까.',
|
|
],
|
|
intake: [
|
|
'요청서 들어왔다.',
|
|
'한번 읽어보자.',
|
|
'오케이, 뭘 원하시는지 보자.',
|
|
],
|
|
analyzing: [
|
|
'음… 의도가 조금 모호한데?',
|
|
'맥락부터 정리해보자.',
|
|
'핵심이 뭐였더라.',
|
|
'이거 작업 범위가 어디까지지?',
|
|
],
|
|
need_clarification: [
|
|
'이건 사용자 확인이 먼저야.',
|
|
'질문 하나만 하고 가자.',
|
|
'추측으로 가면 위험해.',
|
|
'핵심 정보가 빠졌네.',
|
|
],
|
|
contract_ready: [
|
|
'좋아, 작업 조건 정리 완료.',
|
|
'이제 방향은 잡혔어.',
|
|
'계약서 도장 찍었다.',
|
|
'이제 진짜 시작.',
|
|
],
|
|
planning: [
|
|
'순서부터 잡아보자.',
|
|
'기존 기능은 건드리지 말자.',
|
|
'화이트보드 좀 빌릴게.',
|
|
'단계 나눠서 가자.',
|
|
],
|
|
executing: [
|
|
'코드 들어간다.',
|
|
'이번엔 단순하게 가자.',
|
|
'집중 모드 진입.',
|
|
'키보드 워밍업 완료.',
|
|
],
|
|
reviewing: [
|
|
'잠깐, 이건 다시 보자.',
|
|
'기존 기능 깨지는지 확인해야 해.',
|
|
'검수자 시점으로 한 번 더.',
|
|
'엣지 케이스 빠진 거 없나.',
|
|
],
|
|
waiting_approval: [
|
|
'이건 승인 없이 못 바꿔.',
|
|
'위험 작업 감지. 확인 필요!',
|
|
'사장님 결재 부탁드립니다.',
|
|
'도장 받기 전엔 멈춤.',
|
|
],
|
|
error: [
|
|
'앗, 이건 예상 못 했는데…',
|
|
'조건 하나 놓쳤네.',
|
|
'잠깐, 다시 정리.',
|
|
'엇, 이게 깨졌네.',
|
|
],
|
|
done: [
|
|
'좋아, 끝났다!',
|
|
'이번 작업 깔끔하게 완료.',
|
|
'커피 한 잔.',
|
|
'오늘치 끝!',
|
|
],
|
|
};
|
|
|
|
const EVENT_BUBBLE_POOL: Record<AgentEvent, string[]> = {
|
|
missing_required_info: [
|
|
'핵심 정보가 빠졌어.',
|
|
'이거 빠지면 추측해야 해.',
|
|
],
|
|
clarification_needed: [
|
|
'질문 하나만.',
|
|
'확실하지 않으면 먼저 물어보자.',
|
|
],
|
|
requirement_contract_created: [
|
|
'계약서 도장 찍었다.',
|
|
'이제 방향은 잡혔어.',
|
|
],
|
|
plan_completed: [
|
|
'계획 정리 끝!',
|
|
'순서대로 가자.',
|
|
],
|
|
execution_started: [
|
|
'코드 들어간다.',
|
|
'시작!',
|
|
],
|
|
review_failed: [
|
|
'조건 하나 놓쳤네. 다시 보자.',
|
|
'너무 복잡하게 가는 거 아냐?',
|
|
],
|
|
review_passed: [
|
|
'검수 통과!',
|
|
'셋 다 만족이래.',
|
|
],
|
|
risky_change_detected: [
|
|
'잠깐, 이건 승인 필요!',
|
|
'파일 삭제는 함부로 하면 안 돼.',
|
|
],
|
|
approval_required: [
|
|
'결재 부탁드립니다.',
|
|
'도장 받고 이어 갈게.',
|
|
],
|
|
error_occurred: [
|
|
'앗, 이건 예상 못 했는데…',
|
|
'엇, 이게 깨졌네.',
|
|
],
|
|
task_completed: [
|
|
'좋아, 끝났다!',
|
|
'오늘치 끝.',
|
|
],
|
|
stage_loop_retry: [
|
|
'한 번 더 가자.',
|
|
'버그 잡고 다시 시도.',
|
|
],
|
|
};
|
|
|
|
/**
|
|
* 텍스트 풀에서 하나 뽑기. 같은 결과가 연달아 나오지 않게 lastPicked를
|
|
* 받아 회피. 풀이 1개뿐이면 어쩔 수 없이 그걸 반환.
|
|
*/
|
|
export function pickBubbleText(pool: string[], lastPicked?: string): string {
|
|
if (!pool || pool.length === 0) return '';
|
|
if (pool.length === 1) return pool[0];
|
|
let candidates = pool;
|
|
if (lastPicked) candidates = pool.filter((s) => s !== lastPicked);
|
|
if (candidates.length === 0) candidates = pool;
|
|
return candidates[Math.floor(Math.random() * candidates.length)];
|
|
}
|
|
|
|
export function getStatusBubbleText(status: AgentStatus, lastPicked?: string): string {
|
|
return pickBubbleText(STATUS_BUBBLE_POOL[status] ?? [], lastPicked);
|
|
}
|
|
|
|
export function getEventBubbleText(event: AgentEvent, lastPicked?: string): string {
|
|
return pickBubbleText(EVENT_BUBBLE_POOL[event] ?? [], lastPicked);
|
|
}
|
|
|
|
/** 이벤트 → 말풍선 type 매핑. 색상/스타일을 webview가 결정할 때 사용. */
|
|
export function eventBubbleType(event: AgentEvent): BubbleType {
|
|
switch (event) {
|
|
case 'error_occurred': return 'error';
|
|
case 'review_failed':
|
|
case 'risky_change_detected':
|
|
case 'missing_required_info':
|
|
return 'warning';
|
|
case 'task_completed':
|
|
case 'review_passed':
|
|
case 'plan_completed':
|
|
case 'requirement_contract_created':
|
|
return 'success';
|
|
default: return 'event';
|
|
}
|
|
}
|
|
|
|
/** AgentBubble factory — id 자동 생성. */
|
|
export function makeBubble(opts: {
|
|
agentId: string;
|
|
text: string;
|
|
type?: BubbleType;
|
|
durationMs?: number;
|
|
}): AgentBubble {
|
|
return {
|
|
id: `b-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
|
|
agentId: opts.agentId,
|
|
text: opts.text,
|
|
type: opts.type ?? 'status',
|
|
createdAt: Date.now(),
|
|
durationMs: opts.durationMs,
|
|
};
|
|
}
|