v2.2.292-296: 메모리·발열 최적화 + Pixel Office 완전 제거 + 소형 모델 추론 강화(calculate/run_code/지식 스코프)

- v2.2.292 메모리·발열: 헬스체크 워크스페이스 쓰기→fs.access(워처 연쇄 제거)·git 검사 비동기 30분 주기,
  웹뷰 retainContextWhenHidden 정리(채팅만 유지), 브레인 인덱스 유휴 30분 TTL 해제,
  채팅 DOM 200개 상한, @lmstudio/sdk 지연 로드
- v2.2.293 Pixel Office 시각화 폐기: astraOffice 모듈·사이드바 매니저 3종·스프라이트 17MB 삭제,
  sidebarProvider collector 섹션 통삭제 (기업 모드 판단 로직 무변경, vsix 12.2MB→1.3MB)
- v2.2.294 도구 메뉴 'NotebookLM 백엔드 실행/종료' 버튼: OS 자동 감지, 프로젝트 자동 탐색+폴더 선택 저장,
  포트 정리 폴백 (터미널 없이 Research 백엔드 켜고 끄기)
- v2.2.295 추론 강화 1탄: [ACTION 16] <calculate> Python 계산 위임(결과 재주입·자가수정 루프),
  단계별 지식 스코프(General+Specialty, 결정적 도메인 분류, 설정 지식·기억 탭 UI)
- v2.2.296 추론 강화 2탄: [ACTION 17] <run_code> 실행 확인(stdout 회수→수정→재실행 루프),
  문법 오류 재주입 자가수정, 도구 라우팅 힌트(dynamicBlocks), Grounding rescue 도메인 스코프 확장
- 검증: jest 818 통과(신규 29개), tsc 무오류, esbuild 정상

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 18:59:47 +09:00
co-authored by Claude Fable 5
parent fa397d7fa1
commit bb7cd6b879
466 changed files with 1285 additions and 6588 deletions
+4 -514
View File
@@ -26,21 +26,13 @@ import { handleChatMessage } from './sidebar/chatHandlers';
import { handleBrainMessage } from './sidebar/brainHandlers';
import { handleChronicleMessage } from './sidebar/chronicleHandlers';
import { handleAgentMessage } from './sidebar/agentHandlers';
import { handleBridgeLauncherMessage } from './features/datacollect/bridgeLauncher';
import { registerSidebarHandler, dispatchSidebarMessage } from './sidebar/registry';
import { ApprovalGateManager } from './sidebar/managers/approvalGates';
import { ArchitectureWatchManager } from './sidebar/managers/architectureWatch';
import { CompanyTurnRuntime, AlignmentSession } from './sidebar/managers/companyTurn';
import { PixelOfficeState } from './sidebar/managers/pixelOfficeState';
import {
mergeAgentWorkState,
summariseContract,
buildOfficeRoster,
buildPixelOfficeUpdatePayload,
buildPixelOfficeHtml,
} from './sidebar/managers/pixelOfficeHelpers';
import { ChatSessionStore, ChatSession } from './sidebar/managers/chatSessionStore';
import { ModelDiscovery } from './sidebar/managers/modelDiscovery';
import { PixelOfficeLayoutStore } from './sidebar/managers/pixelOfficeLayoutStore';
import {
buildBrainStatusPayload,
buildBrainProfilesPayload,
@@ -107,6 +99,8 @@ registerSidebarHandler(handleChatMessage);
registerSidebarHandler(handleBrainMessage);
registerSidebarHandler(handleChronicleMessage);
registerSidebarHandler(handleAgentMessage);
// NotebookLM 로컬 백엔드 실행/종료 버튼 (도구 메뉴) — Research 기능용 편의 런처.
registerSidebarHandler(handleBridgeLauncherMessage);
import { resolveScopeForAgent } from './skills/agentKnowledgeMap';
import { clearBrainTokenIndex } from './retrieval/brainIndex';
import { extractUrls } from './features/web/webFetch';
@@ -133,7 +127,6 @@ import {
} from './features/company';
import { AIService } from './core/services';
import { HealthCheckMonitor } from './core/health';
import { presentOfficeSnapshot } from './features/astraOffice';
export interface SidebarLmStudioDeps {
lifecycle: ModelLifecycleManager;
@@ -165,15 +158,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
static readonly lastAutoChronicleSignatureStateKey = 'g1nation.lastAutoChronicleSignature';
_view?: vscode.WebviewView;
_panel?: vscode.WebviewPanel;
/**
* Pixel Office 의 모든 raw state slot 을 한 컨테이너로 통합.
* - panel: 전체보기 webview panel reference (닫히면 undefined)
* - current: 누적된 AgentWorkState (broadcast 직전 patch merge 결과)
* - lastBubble Map: 같은 텍스트 연속 출력 회피
* - activity ring buffer: officeSnapshot 의 action-tag 히스토리
* Broadcast 메서드는 provider 에 남아 webview send 와 직접 통신.
*/
private readonly _pixelOffice = new PixelOfficeState();
public brainEnabled = true;
_currentSessionBrainId: string | null = null;
_currentNegativePrompt: string = '';
@@ -228,484 +212,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
/** 옛 _pendingAlignment slot 을 AlignmentSession 으로 통합. */
private readonly _alignment = new AlignmentSession();
/**
* Pixel Office 현재 작업 상태 캐시. 모든 emit/alignment hook이 한 슬롯에
* 모인 상태를 patch 형식으로 갱신 → broadcast. UI layer이므로 어떤 판단
* 로직에도 다시 영향 주지 않는다 — 단방향 read-only 흐름.
*/
// _pixelOfficeState / _pixelOfficeLastBubble / _pixelOfficeActivity / PIXEL_OFFICE_ACTIVITY_MAX
// 는 _pixelOffice (PixelOfficeState) 컨테이너로 통합.
/**
* Astra Office 사용자 정의 레이아웃. workspace state에 저장되어 프로젝트별
* 다른 배치를 가질 수 있다. null/undefined면 webview가 디폴트 LAYOUT을 사용.
* 편집 모드에서 사용자가 드래그로 위치 조정 후 저장 → 이 슬롯 업데이트 →
* webview에 다시 broadcast.
*
* 데이터 shape:
* {
* cells: [{ roleKey, deskX, deskY, charX, charY }, ...],
* decos: [{ id, type, x, y }, ...]
* }
*/
// Pixel Office layout 영구 저장 — `src/sidebar/managers/pixelOfficeLayoutStore.ts` 의
// PixelOfficeLayoutStore 로 이관. constructor 에서 초기화.
/** Phase B-2에서 chatHandlers가 alignment 진행 여부를 빠르게 확인하는 용도. */
isAlignmentPending(): boolean {
return this._alignment.isPending();
}
// ─────────────────────── Pixel Office collector ───────────────────────
//
// 이 섹션은 *기존 emit / alignment / classifier 결과를 가로채* 그대로
// 모니터링용 상태로 변환하는 단방향 hub. 어떤 메서드도 추가 LLM 콜을
// 만들지 않고, dispatcher / planner / chatHandlers의 어느 분기에도
// 영향을 주지 않는다. 그래서 이 섹션 전체를 통째로 지워도 회사 모드는
// 평소와 동일하게 동작해야 한다 — 그 invariant가 깨지면 위반이다.
/** webview로 보낼 직전 patch와 가벼운 reset/유틸. */
private _pixelOfficeBroadcast(patch: Partial<import('./features/company').AgentWorkState>, opts?: {
bubbleStatus?: import('./features/company').AgentStatus;
bubbleEvent?: import('./features/company').AgentEvent;
bubbleAgentId?: string;
}): void {
const cfg = getConfig();
if (!cfg.companyPixelOfficeEnabled) return;
const next = mergeAgentWorkState(this._pixelOffice.current, patch);
this._pixelOffice.current = next;
// 말풍선 — 상태 또는 이벤트가 지정된 경우만 생성. 풀에서 직전에 쓴 텍스트
// 회피해서 같은 말 연달아 안 나오게.
const bubbles: import('./features/company').AgentBubble[] = [];
if (cfg.companyPixelOfficeBubbles) {
const aid = opts?.bubbleAgentId ?? next.agentId;
if (opts?.bubbleStatus) {
const lastKey = `status:${opts.bubbleStatus}`;
// 동적 import 대신 require로 — 메서드 내부에서 너무 늦게 await 걸지 않게.
const { getStatusBubbleText, makeBubble } = require('./features/company') as typeof import('./features/company');
const text = getStatusBubbleText(opts.bubbleStatus, this._pixelOffice.getLastBubble(lastKey));
if (text) {
this._pixelOffice.setLastBubble(lastKey, text);
bubbles.push(makeBubble({ agentId: aid, text, type: 'status' }));
}
}
if (opts?.bubbleEvent) {
const lastKey = `event:${opts.bubbleEvent}`;
const { getEventBubbleText, eventBubbleType, makeBubble } = require('./features/company') as typeof import('./features/company');
const text = getEventBubbleText(opts.bubbleEvent, this._pixelOffice.getLastBubble(lastKey));
if (text) {
this._pixelOffice.setLastBubble(lastKey, text);
bubbles.push(makeBubble({ agentId: aid, text, type: eventBubbleType(opts.bubbleEvent) }));
}
}
}
const payload = {
type: 'pixelOfficeUpdate' as const,
value: {
state: next,
bubbles,
config: {
enabled: cfg.companyPixelOfficeEnabled,
bubblesEnabled: cfg.companyPixelOfficeBubbles,
maxVisibleBubbles: 3,
bubbleDurationMs: 4500,
},
},
};
// 사이드바 mini panel + 전체보기 webview panel 둘 다 같은 데이터 받음.
this._view?.webview.postMessage(payload);
this._pixelOffice.panel?.webview.postMessage(payload);
// ── refactor #4 단계적 마이그레이션 ──
// 옛 `pixelOfficeUpdate` 와 함께 새 `officeSnapshot` 도 emit. 어느 webview 가
// 새 message 를 listen 하기 시작하면 그때부터 자연스럽게 OfficeSnapshot 기반으로
// 동작. 양쪽 다 listen 안 해도 옛 동작 유지. 다음 세션에 webview 측 리시버 추가.
try {
// Roster derive (#G): 현재 turn 의 active agents 전체를 OfficeSnapshot 에 포함.
// listActiveAgentsByCategory 는 CEO 를 항상 포함하고 사용자 activeAgentIds 만
// 통과시킨다. presenter 가 active agent 만 실제 상태로 표시, 나머지는 idle.
const roster = buildOfficeRoster(readCompanyState(this._context));
const snapshot = presentOfficeSnapshot({
activeState: next,
recentBubbles: bubbles,
recentActivity: this._pixelOffice.getActivity(),
roster,
});
const snapMsg = { type: 'officeSnapshot' as const, value: snapshot };
this._view?.webview.postMessage(snapMsg);
this._pixelOffice.panel?.webview.postMessage(snapMsg);
} catch (e) {
// presenter 가 throw 해도 옛 동작에는 영향 없도록 swallow.
// 디버깅 필요 시 console.warn 으로 띄울 수 있음.
}
}
/**
* 현재 CompanyState 에서 active agents 를 평탄화 → presenter 의 roster 입력 모양으로.
* read 실패 / company off → 빈 배열 (presenter 가 옛 single-agent fallback 사용).
*/
// _buildOfficeRoster / _pixelOfficeAppendLog → `src/sidebar/managers/pixelOfficeHelpers.ts`
// + PixelOfficeState.appendLog. Activity push 도 PixelOfficeState.pushActivity 직접 호출.
/** Intent classifier가 분류 직후 호출되어 한 줄 상태 진입을 표시. */
pixelOfficeOnIntentClassified(intent: 'chat' | 'followup' | 'new_task', userPrompt: string): void {
if (intent !== 'new_task') {
// 잡담/후속 — 회사 모드 표면적으로 일 안 하므로 'idle'로 잠시 복귀.
this._pixelOfficeBroadcast({
status: 'idle',
currentTask: userPrompt,
currentStep: intent === 'followup' ? '직전 라운드 후속 응답' : '잡담 응답',
message: undefined,
recentLogs: this._pixelOffice.appendLog(`💬 분류: ${intent}`),
}, { bubbleStatus: 'idle' });
return;
}
// new_task — intake → analyzing 흐름의 첫 신호.
this._pixelOfficeBroadcast({
status: 'intake',
currentTask: userPrompt,
currentStep: '요청 접수',
recentLogs: this._pixelOffice.appendLog(`📨 새 작업 요청 접수`),
}, { bubbleStatus: 'intake' });
}
/** Intent Alignment 분석 시작 (LLM 콜 직전). */
pixelOfficeOnAlignmentStart(userPrompt: string): void {
this._pixelOfficeBroadcast({
status: 'analyzing',
currentTask: this._pixelOffice.current?.currentTask || userPrompt,
currentStep: '요청 분석 중 (C·G·C·F 추출)',
message: undefined,
recentLogs: this._pixelOffice.appendLog('🔍 의도 분석 시작'),
}, { bubbleStatus: 'analyzing' });
}
/** Alignment 결과를 받아 카드로 표시 직전. kind=auto-proceed면 곧장 planning 흐름. */
pixelOfficeOnAlignmentResult(kind: 'questions' | 'confirm' | 'auto-proceed', contract: import('./features/company').RequirementContract): void {
if (kind === 'auto-proceed') {
this._pixelOfficeBroadcast({
status: 'contract_ready',
currentStep: 'Requirement Contract 확정 (자동 진행)',
requirementContract: summariseContract(contract),
needUserInput: undefined,
recentLogs: this._pixelOffice.appendLog('✅ 계약 자동 확정'),
}, {
bubbleStatus: 'contract_ready',
bubbleEvent: 'requirement_contract_created',
});
return;
}
if (kind === 'questions') {
this._pixelOfficeBroadcast({
status: 'need_clarification',
currentStep: '확인 질문 대기 중',
requirementContract: summariseContract(contract),
needUserInput: contract.openQuestions,
recentLogs: this._pixelOffice.appendLog(`🤔 ${contract.openQuestions.length}개 질문 대기`),
}, {
bubbleStatus: 'need_clarification',
bubbleEvent: 'clarification_needed',
});
return;
}
// confirm — 질문 없음, 사용자 OK만 받으면 됨.
this._pixelOfficeBroadcast({
status: 'waiting_approval',
currentStep: '계약 확인 대기',
requirementContract: summariseContract(contract),
needUserInput: undefined,
awaitingApproval: '사용자 확인 — 그대로 진행할지 여부',
recentLogs: this._pixelOffice.appendLog('🧭 계약 확인 카드 표시'),
}, { bubbleStatus: 'waiting_approval' });
}
/** Alignment 취소(사용자가 카드 닫음). */
pixelOfficeOnAlignmentCancelled(): void {
this._pixelOfficeBroadcast({
status: 'idle',
currentStep: '취소됨',
needUserInput: undefined,
awaitingApproval: undefined,
recentLogs: this._pixelOffice.appendLog('🛑 작업 취소'),
});
}
// _summariseContract → `src/sidebar/managers/pixelOfficeHelpers.ts`
/**
* Dispatcher의 `CompanyTurnEvent`를 그대로 받아 Pixel Office 상태로 변환.
* `_runCompanyTurn`의 emit 콜백이 매 이벤트를 한 번 더 이쪽으로 흘려준다.
* 함수 안에서 어떤 dispatcher 분기도 다시 트리거하지 않는다 — read-only.
*/
pixelOfficeOnTurnEvent(ev: import('./features/company').CompanyTurnEvent): void {
switch (ev.phase) {
case 'plan-start':
this._pixelOfficeBroadcast({
status: 'planning',
currentStep: '계획 수립',
recentLogs: this._pixelOffice.appendLog('📋 plan 작성'),
}, { bubbleStatus: 'planning' });
return;
case 'plan-ready': {
// B. 미니 맵용 stage 리스트 초기화. 모두 pending.
const stages = (ev.plan?.tasks ?? []).map((t) => ({
label: (t.task || '').slice(0, 24) || '단계',
agent: t.agent,
status: 'pending' as const,
}));
this._pixelOfficeBroadcast({
status: 'planning',
currentStep: '계획 완료',
nextStep: ev.plan?.tasks?.[0]?.task,
message: ev.plan?.brief?.slice(0, 120),
recentLogs: this._pixelOffice.appendLog(`📋 plan 완료 (${ev.plan?.tasks?.length ?? 0}개 task)`),
pipelineStages: stages,
}, { bubbleEvent: 'plan_completed' });
return;
}
case 'agent-start': {
// B. 현재 stage를 active로, 이전 stage들은 done으로.
const prev = this._pixelOffice.current?.pipelineStages;
const stages = prev ? prev.map((s, i) => ({
...s,
status: (i < ev.index ? 'done' : i === ev.index ? 'active' : 'pending') as 'done'|'active'|'pending',
})) : undefined;
this._pixelOfficeBroadcast({
status: 'executing',
currentStep: ev.task?.slice(0, 80),
nextStep: undefined,
message: `${ev.agentId} • ${ev.index + 1}/${ev.total}`,
progress: ev.total > 0 ? ev.index / ev.total : undefined,
recentLogs: this._pixelOffice.appendLog(`▶ ${ev.agentId} start (${ev.index + 1}/${ev.total})`),
pipelineStages: stages,
}, {
bubbleAgentId: ev.agentId,
bubbleStatus: 'executing',
bubbleEvent: ev.index === 0 ? 'execution_started' : undefined,
});
return;
}
case 'agent-done':
// E. action-tag report가 있으면 ticker로 별도 push — 캐릭터가 *무슨
// 일을 했는지* 실시간 가시화. 사용자가 신뢰도/투명성을 즉시 체감.
if (Array.isArray(ev.output?.actionReport) && ev.output.actionReport.length > 0) {
const items = ev.output.actionReport.map((line: string) => ({
agentId: ev.agentId, text: line, ts: Date.now(),
}));
this._view?.webview.postMessage({ type: 'pixelOfficeActivity', value: { items } });
this._pixelOffice.panel?.webview.postMessage({ type: 'pixelOfficeActivity', value: { items } });
// 새 path 도 동기화 — ring buffer 에 누적 → 다음 officeSnapshot 에 포함.
this._pixelOffice.pushActivity(items.map((it) => ({ agentId: it.agentId, text: it.text })));
}
this._pixelOfficeBroadcast({
status: ev.output?.error ? 'error' : 'executing',
progress: ev.total > 0 ? (ev.index + 1) / ev.total : undefined,
recentLogs: this._pixelOffice.appendLog(
ev.output?.error
? `❌ ${ev.agentId} ${ev.output.error}`
: `✓ ${ev.agentId} 완료`,
),
}, {
bubbleAgentId: ev.agentId,
bubbleEvent: ev.output?.error ? 'error_occurred' : undefined,
});
return;
case 'stage-loop':
this._pixelOfficeBroadcast({
status: 'executing',
currentStep: `재시도: ${ev.from} → ${ev.to} (${ev.iteration}회)`,
recentLogs: this._pixelOffice.appendLog(`🔁 loop ${ev.from}→${ev.to} #${ev.iteration}`),
}, { bubbleEvent: 'stage_loop_retry' });
return;
case 'review-start':
this._pixelOfficeBroadcast({
status: 'reviewing',
currentStep: `검수 사이클 — ${ev.stageLabel}`,
message: `검수자: ${ev.inspectorAgentId} · 최대 ${ev.maxRounds}라운드`,
recentLogs: this._pixelOffice.appendLog(`🔍 검수 시작: ${ev.stageLabel}`),
}, { bubbleStatus: 'reviewing' });
return;
case 'review-round':
this._pixelOfficeBroadcast({
status: 'reviewing',
currentStep: `검수 라운드 ${ev.round} (${ev.inspectorVerdict}/${ev.ceoVerdict})`,
recentLogs: this._pixelOffice.appendLog(
`R${ev.round} insp:${ev.inspectorVerdict} ceo:${ev.ceoVerdict}`,
),
}, {
bubbleAgentId: ev.inspectorAgentId,
bubbleEvent: ev.inspectorVerdict === 'pass' && ev.ceoVerdict === 'pass'
? 'review_passed'
: (ev.inspectorVerdict === 'revise' ? 'review_failed' : undefined),
});
return;
case 'review-end':
this._pixelOfficeBroadcast({
status: ev.final === 'aborted' ? 'error' : 'executing',
currentStep: ev.final === 'pass'
? `검수 통과 (${ev.rounds}라운드)`
: ev.final === 'maxed-out'
? `검수 한도 도달 — 진행 (${ev.rounds}라운드)`
: '검수 중단',
recentLogs: this._pixelOffice.appendLog(`🔍 검수 종료: ${ev.final}`),
}, {
bubbleEvent: ev.final === 'pass' ? 'review_passed'
: ev.final === 'aborted' ? 'risky_change_detected'
: undefined,
});
return;
case 'awaiting-approval':
this._pixelOfficeBroadcast({
status: 'waiting_approval',
currentStep: `승인 대기 — ${ev.stageLabel}`,
awaitingApproval: `${ev.stageLabel} 단계 완료 검토`,
recentLogs: this._pixelOffice.appendLog(`✋ 승인 대기: ${ev.stageLabel}`),
}, {
bubbleStatus: 'waiting_approval',
bubbleEvent: 'approval_required',
});
return;
case 'approval-resolved':
this._pixelOfficeBroadcast({
status: 'executing',
awaitingApproval: undefined,
currentStep: `승인 결과: ${ev.decision}`,
recentLogs: this._pixelOffice.appendLog(`✋→ ${ev.decision}`),
});
return;
case 'report-start':
this._pixelOfficeBroadcast({
status: 'reviewing',
currentStep: 'CEO 종합 보고서 작성 중',
recentLogs: this._pixelOffice.appendLog('🧭 보고서 작성'),
});
return;
case 'report-done':
this._pixelOfficeBroadcast({
status: 'done',
currentStep: ev.ok ? '보고서 완료' : '보고서 (fallback) 완료',
progress: 1,
recentLogs: this._pixelOffice.appendLog(ev.ok ? '✅ 보고서 OK' : '⚠ fallback 보고서'),
}, {
bubbleStatus: 'done',
bubbleEvent: 'task_completed',
});
return;
case 'session-saved':
this._pixelOfficeBroadcast({
status: 'done',
message: `세션 저장됨`,
recentLogs: this._pixelOffice.appendLog('💾 세션 저장'),
});
return;
case 'aborted':
this._pixelOfficeBroadcast({
status: 'error',
currentStep: `중단: ${ev.reason}`,
recentLogs: this._pixelOffice.appendLog(`🛑 abort: ${ev.reason}`),
}, {
bubbleStatus: 'error',
bubbleEvent: 'error_occurred',
});
return;
case 'telegram-mirror':
default:
return; // 시각화에 의미 약함 — log skip
}
}
/** webview가 처음 로드되거나 사용자가 토글을 다시 켰을 때 캐시된 상태 재전송. */
pixelOfficeResend(): void {
const cfg = getConfig();
const payload = buildPixelOfficeUpdatePayload(cfg, this._pixelOffice.current);
this._view?.webview.postMessage(payload);
this._pixelOffice.panel?.webview.postMessage(payload);
if (cfg.companyPixelOfficeEnabled) {
try {
const snapshot = presentOfficeSnapshot({
activeState: payload.value.state ?? undefined,
recentBubbles: [],
recentActivity: this._pixelOffice.getActivity(),
roster: buildOfficeRoster(readCompanyState(this._context)),
});
const snapMsg = { type: 'officeSnapshot' as const, value: snapshot };
this._view?.webview.postMessage(snapMsg);
this._pixelOffice.panel?.webview.postMessage(snapMsg);
} catch {
// Snapshot 재전송 실패가 기존 fallback 경로를 깨면 안 됨.
}
}
}
/**
* editor area에 별도 Pixel Office 전체보기 panel을 띄움. 이미 열려 있으면
* 그 panel을 reveal. 사이드바 mini 패널과 동일한 데이터 스트림을 받지만
* 별도 HTML로 *사무실 그리드 + 직군별 캐릭터* 레이아웃을 보여준다.
*/
public openPixelOfficePanel(column: vscode.ViewColumn = vscode.ViewColumn.Beside): vscode.WebviewPanel {
if (this._pixelOffice.panel) {
this._pixelOffice.panel.reveal(column);
return this._pixelOffice.panel;
}
const panel = vscode.window.createWebviewPanel(
'astra.pixelOffice',
'Astra Office',
column,
{ enableScripts: true, localResourceRoots: [this._extensionUri], retainContextWhenHidden: true },
);
this._pixelOffice.panel = panel;
panel.webview.html = buildPixelOfficeHtml(this._extensionUri, panel.webview);
// panel과 백엔드 사이의 메시지 채널 — 닫기/리프레시 + 레이아웃 편집.
panel.webview.onDidReceiveMessage((msg: any) => {
if (!msg || typeof msg !== 'object') return;
if (msg.type === 'getPixelOfficeState') this.pixelOfficeResend();
if (msg.type === 'closePixelOfficePanel') panel.dispose();
if (msg.type === 'getPixelOfficeLayout') {
// webview가 layout 요청 — 저장된 게 있으면 그것, 없으면 null.
panel.webview.postMessage({
type: 'pixelOfficeLayoutLoaded',
value: this._layoutStore.read() ?? null,
});
}
if (msg.type === 'savePixelOfficeLayout') {
// webview의 편집 모드에서 사용자가 저장 버튼 누름.
void this._layoutStore.write(msg.value).then(() => {
panel.webview.postMessage({
type: 'pixelOfficeLayoutSaved', value: { ok: true },
});
});
}
if (msg.type === 'resetPixelOfficeLayout') {
// 디폴트 LAYOUT으로 복귀.
void this._layoutStore.clear().then(() => {
panel.webview.postMessage({
type: 'pixelOfficeLayoutSaved', value: { ok: true, reset: true },
});
});
}
if (msg.type === 'pixelOfficeCommand') {
// D. 캐릭터 컨텍스트 메뉴 액션. 현재 abort만 처리 — Stop 버튼과
// 동일 경로(abortCompanyTurn + alignment 취소 + agent.stop)로
// 작업 즉시 중단. skip/pause 등은 dispatcher에 별도 API가 없어 미지원.
if (msg.cmd === 'abort') {
this.abortCompanyTurn();
this.cancelPendingAlignment();
this._agent.stop();
}
}
});
panel.onDidDispose(() => {
if (this._pixelOffice.panel === panel) this._pixelOffice.panel = undefined;
});
// 열자마자 현재 상태 한 번 push.
this.pixelOfficeResend();
return panel;
}
// _buildPixelOfficeHtml → `src/sidebar/managers/pixelOfficeHelpers.ts` (buildPixelOfficeHtml)
/** Alignment 슬롯 비우기 — 사용자가 "취소"를 눌렀거나 turn 시작/종료 시점 호출. */
clearPendingAlignment(): void {
this._alignment.clear();
@@ -723,8 +236,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
/** chat_sessions globalState 의 read/write/normalize 를 한 클래스로 격리. constructor 에서 초기화. */
private readonly _sessionStore: ChatSessionStore;
/** Pixel Office 사용자 정의 layout 의 workspaceState read/write/clear. constructor 에서 초기화. */
private readonly _layoutStore: PixelOfficeLayoutStore;
/** Chronicle 프로젝트 목록 globalState read/write + workspace fallback. constructor 에서 초기화. */
private readonly _chronicleStore: ChronicleProjectStore;
/** Agent skill .md 파일 + lastSelected + negativePrompt 영구 저장. constructor 에서 초기화. */
@@ -742,7 +253,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
readonly _lmStudio?: SidebarLmStudioDeps
) {
this._sessionStore = new ChatSessionStore(_context);
this._layoutStore = new PixelOfficeLayoutStore(_context);
this._chronicleStore = new ChronicleProjectStore(_context);
this._agentSkillStore = new AgentSkillStore(_context);
this._sessionState = new SessionStateStore(_context);
@@ -2028,9 +1538,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
const state = readCompanyState(this._context);
const activePipeline = resolveActivePipeline(state);
// Pixel Office: 분석 시작 표시 (LLM 콜 직전).
try { this.pixelOfficeOnAlignmentStart(opts.userPrompt); } catch { /* noop */ }
// 모드 전환 직전 일반 채팅 요약 — Intent Alignment 가 *이미 논의된 맥락* 을
// 재질문하지 않도록. 후속 라운드(previousContract 있음) 면 chatHistory 가
// 이미 contract 에 흡수됐으므로 중복 첨부 안 함.
@@ -2126,9 +1633,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
],
openQuestions: contract.openQuestions.filter((q) => !solvedSet.has(q)),
};
this._pixelOfficeBroadcast({
recentLogs: this._pixelOffice.appendLog(`🔎 자가 조사로 질문 ${solved.length}건 해결`),
});
}
}
} catch { /* 자가 조사 실패 — 원래 질문 그대로 노출 */ }
@@ -2137,7 +1641,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
if (shouldAutoProceedAlignment(opts.mode, contract, reachedLimit)) {
// contract 한 줄 안내 후 곧장 pipeline. 사용자 friction 최소.
this._view?.webview.postMessage(buildAlignmentAutoProceedPayload(contract, reachedLimit));
try { this.pixelOfficeOnAlignmentResult('auto-proceed', contract); } catch { /* noop */ }
this._alignment.clear();
this._saveAlignmentKnowledgeIfAny(opts.userPrompt, contract.answeredQuestions);
await this._runCompanyTurn(opts.userPrompt, undefined, opts.pipelineIdOverride, contract);
@@ -2149,7 +1652,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
const askMode: 'confirm' | 'questions' = reachedLimit || contract.openQuestions.length === 0
? 'confirm' : 'questions';
this._view?.webview.postMessage(buildAlignmentAskPayload(askMode, contract, opts.roundsAsked, opts.roundsLimit));
try { this.pixelOfficeOnAlignmentResult(askMode, contract); } catch { /* noop */ }
this._alignment.set({
userOriginalPrompt: opts.userPrompt,
contract,
@@ -2224,12 +1726,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
if (cfg.companyAlignmentKnowledgeSave === false) return;
if (!qaList || qaList.length === 0) return;
const brain = getActiveBrainProfile();
const saved = saveAlignmentKnowledge(brain.localBrainPath, { userPrompt, qaList });
if (saved) {
this._pixelOfficeBroadcast({
recentLogs: this._pixelOffice.appendLog('💾 확인된 정보 두뇌 저장'),
});
}
saveAlignmentKnowledge(brain.localBrainPath, { userPrompt, qaList });
} catch { /* non-fatal */ }
}
@@ -2242,7 +1739,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
this._alignment.clear();
this._view?.webview.postMessage(buildAlignmentCancelledPayload());
this._postStreamEnd();
try { this.pixelOfficeOnAlignmentCancelled(); } catch { /* noop */ }
void this._sendReadyStatus();
}
@@ -2335,7 +1831,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
const emitter = createCompanyTurnEmit({
postWebview: (msg) => { this._view?.webview.postMessage(msg); },
recordSummary: (brief, tail) => this._companyTurn.recordSummary(brief, tail),
onPixelOffice: (event) => this.pixelOfficeOnTurnEvent(event),
});
// Fresh AbortController per turn — the Stop button routes through
// `abortCompanyTurn()` to fire `.abort()`. The dispatcher checks
@@ -2360,9 +1855,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
if (cfgTurn.webAutoFetchEnabled !== false && !resumeTimestamp) {
const urls = extractUrls(userPrompt, 2);
if (urls.length > 0) {
this._pixelOfficeBroadcast({
recentLogs: this._pixelOffice.appendLog(`🌐 URL ${urls.length}건 수집 중`),
});
const blocks: string[] = [];
for (const url of urls) {
blocks.push(await buildUrlContext(url)); // 5분 캐시 — alignment 와 중복 비용 없음
@@ -3445,5 +2937,3 @@ export function wrapPanelAsView(panel: vscode.WebviewPanel): vscode.WebviewView
return adapter as vscode.WebviewView;
}
// Astra Office full-panel HTML 은 src/features/astraOffice/ 로 이전됨 (refactor #1).
// _buildPixelOfficeHtml() 가 renderAstraOfficePanelHtml() 을 직접 호출한다.