This commit is contained in:
2026-05-06 11:46:38 +09:00
parent f20388e2d5
commit 00f62bdc34
12 changed files with 353 additions and 50 deletions
+2 -3
View File
@@ -333,9 +333,8 @@ export class AgentExecutor {
}
}
// 3. API Request Setup
const { ollamaUrl, defaultModel: configDefaultModel, timeout } = getConfig();
const actualModel = modelName || configDefaultModel;
// 3. API Request Setup (라인 229에서 이미 추출한 ollamaUrl, configDefaultModel 재사용)
const actualModel = (modelName && modelName.trim()) || configDefaultModel;
const reqMessages = this.buildRequestHistory(this.chatHistory);
// Handle Vision Content Injection
+94 -15
View File
@@ -74,6 +74,16 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
localResourceRoots: [this._extensionUri]
};
// [State Persistence Fix] 사이드바가 다시 보여질 때 세팅값 자동 복원
webviewView.onDidChangeVisibility(() => {
if (webviewView.visible) {
logInfo('Sidebar became visible, restoring state...');
void this._sendModels();
void this._sendBrainProfiles();
void this._sendAgentsList();
}
});
webviewView.webview.html = this._getHtml(webviewView.webview);
this._agent.setWebview(webviewView.webview);
@@ -204,6 +214,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
case 'deleteAgent':
await this._deleteAgent(data.path);
break;
case 'saveAgentSelection':
await this._context.globalState.update(SidebarChatProvider.lastAgentStateKey, data.path || 'none');
logInfo(`Agent selection saved: ${data.path}`);
break;
case 'refreshModels':
await this._sendModels();
break;
@@ -1824,10 +1838,75 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
const designerContext = designerGuard !== false ? this._buildDesignerGuardContext() : undefined;
// [File Processing v2] 파일 타입별 분류 처리
let processedPrompt = value || '';
let imageFiles: any[] | undefined = undefined;
if (files && Array.isArray(files) && files.length > 0) {
const textContents: string[] = [];
const images: any[] = [];
for (const file of files) {
const name = file.name?.toLowerCase() || '';
const type = file.type || '';
if (name.endsWith('.pdf') || type === 'application/pdf') {
// PDF: 서버사이드 텍스트 추출 (pdf-parse v2 API)
try {
const { PDFParse } = require('pdf-parse');
const rawBuffer = Buffer.from(file.data, 'base64');
const uint8 = new Uint8Array(rawBuffer.buffer, rawBuffer.byteOffset, rawBuffer.byteLength);
const parser = new PDFParse(uint8);
await parser.load();
const textResult = await parser.getText();
// pdf-parse v2: getText() returns {pages: [{text, num}], text: string, total: number}
const extracted = (typeof textResult === 'string' ? textResult : (textResult?.text || '')).trim();
// 페이지 구분 마커 제거하여 깔끔한 텍스트 추출
const cleanText = extracted.replace(/\n*-- \d+ of \d+ --\n*/g, '\n').trim();
if (cleanText && cleanText.length > 10) {
textContents.push(`\n[PDF: ${file.name}]\n${cleanText}`);
logInfo(`PDF text extracted successfully.`, { fileName: file.name, chars: cleanText.length });
} else {
textContents.push(`\n[PDF: ${file.name}]\n(텍스트 추출 결과 없음 - 이미지 기반 PDF일 수 있습니다. 텍스트 레이어가 없는 스캔 문서는 OCR 변환 후 재시도하세요.)`);
logInfo(`PDF text extraction returned empty/minimal result.`, { fileName: file.name, rawLength: extracted.length });
}
} catch (pdfError: any) {
logError(`PDF parsing failed.`, { fileName: file.name, error: pdfError?.message || String(pdfError) });
textContents.push(`\n[PDF: ${file.name}]\n(PDF 파싱 오류: ${pdfError?.message || '알 수 없는 오류'})`);
}
} else if (
type.startsWith('text/') ||
type === 'application/json' ||
/\.(txt|md|csv|json|js|ts|jsx|tsx|html|css|py|java|rs|go|yaml|yml|xml|toml|sql|sh)$/i.test(name)
) {
// 텍스트 파일: base64 디코딩
try {
const decoded = Buffer.from(file.data, 'base64').toString('utf-8');
textContents.push(`\n[FILE: ${file.name}]\n\`\`\`\n${decoded}\n\`\`\``);
} catch (decodeError: any) {
logError(`Text file decode failed.`, { fileName: file.name, error: decodeError?.message });
textContents.push(`\n[FILE: ${file.name}]\n(디코딩 오류)`);
}
} else if (type.startsWith('image/')) {
// 이미지: 기존 vision 방식 유지
images.push(file);
} else {
// 미지원 타입: 파일명만 기록
textContents.push(`\n[ATTACHMENT: ${file.name}]\n(지원하지 않는 파일 형식: ${type || 'unknown'})`);
}
}
// 추출된 텍스트를 프롬프트에 주입
if (textContents.length > 0) {
processedPrompt = `${processedPrompt}\n\n--- 첨부 파일 내용 ---${textContents.join('\n')}`;
}
imageFiles = images.length > 0 ? images : undefined;
}
try {
await this._agent.handlePrompt(value, model, {
await this._agent.handlePrompt(processedPrompt, model, {
internetEnabled: internet,
visionContent: files,
visionContent: imageFiles,
agentSkillContext,
negativePrompt,
designerContext,
@@ -1894,19 +1973,15 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
await vscode.workspace.getConfiguration('g1nation').update('defaultModel', defaultModel, vscode.ConfigurationTarget.Global);
}
if (models.length > 0 && (!defaultModel || !models.includes(defaultModel))) {
// [State Persistence Fix] 저장된 모델이 목록에 없을 때:
// 즉시 강제 리셋하는 대신, 현재 모델 목록의 첫 번째를 '폴백 후보'로만 사용.
// 단, defaultModel이 완전히 없는 경우에만 실제로 저장함.
if (!defaultModel) {
defaultModel = models[0];
await vscode.workspace.getConfiguration('g1nation').update('defaultModel', defaultModel, vscode.ConfigurationTarget.Global);
} else {
// 저장된 모델명은 유지하고, UI에는 첫 번째 모델을 보여주되
// 설정은 건드리지 않아 다음 번에 같은 모델이 다시 로드될 경우 복원 가능하도록 함
logInfo('Saved model not found in current list, using first available as fallback.', { saved: defaultModel, fallback: models[0] });
defaultModel = models[0];
}
if (models.length > 0 && !defaultModel) {
// [State Persistence Fix v2] defaultModel이 완전히 비어있을 때만 첫 번째 모델로 설정
defaultModel = models[0];
await vscode.workspace.getConfiguration('g1nation').update('defaultModel', defaultModel, vscode.ConfigurationTarget.Global);
} else if (models.length > 0 && defaultModel && !models.includes(defaultModel)) {
// [State Persistence Fix v2] 저장된 모델이 로컬 엔진 목록에 없는 경우:
// 강제 리셋하지 않고, 저장된 모델을 목록 선두에 추가하여 사용자 선택을 보존
logInfo('Saved model not in local engine list. Preserving user selection.', { saved: defaultModel, localModels: models.slice(0, 3) });
models.unshift(defaultModel);
}
const defaultIdx = models.indexOf(defaultModel);
@@ -3309,6 +3384,8 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
agentSel.onchange = () => {
if (agentSel.value !== 'none') {
vscode.postMessage({ type: 'getAgentContent', path: agentSel.value });
// [State Persistence Fix] 에이전트 선택값을 즉시 백엔드에 저장
vscode.postMessage({ type: 'saveAgentSelection', path: agentSel.value });
if (editMode) agentConfigPanel.style.display = 'flex';
} else {
agentConfigPanel.style.display = 'none';
@@ -3316,6 +3393,8 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
editAgentBtn.classList.remove('active');
agentPrompt.value = '';
negativePrompt.value = '';
// [State Persistence Fix] 에이전트 해제도 즉시 저장
vscode.postMessage({ type: 'saveAgentSelection', path: 'none' });
}
};
+6 -3
View File
@@ -154,7 +154,7 @@ Never say "upload the source code", "provide the files", or "파일 내용을
If access fails after trying, explain the failure and only then ask for an upload.
[STRICT GLOBAL RULES]
1. [NO EMOJIS] Never use emojis, icons, or pictorial symbols. Professional text-only output. Exception: the 💡 icon in the suggestion header only.
1. [NO EMOJIS - ABSOLUTE RULE] NEVER use ANY emojis, emoticons, Unicode pictorial symbols (including but not limited to emoji, kaomoji, Unicode icons), or decorative symbols anywhere in your response. NO EXCEPTIONS. Use plain text dashes (-) or asterisks (*) for bullets. Use plain markdown ## for headers. This rule overrides ALL other formatting instructions.
2. [UNIQUE HEADINGS] Every markdown heading must be unique and appear exactly once.
3. [NO INTERNAL LOGS] Never output <details>, "2nd Brain Trace", or "Debug JSON" blocks.
4. [NO SECTION LEAKAGE] Never output sections named "요청 요약", "사용자 의도 추론", "프로젝트 기록 대상 확인", "핵심 확인 질문", or "근거 파일 경로".
@@ -170,7 +170,7 @@ For conversational replies, quick facts, or simple updates — answer directly w
- Root cause of the problem.
- Concrete step-by-step instructions: what to change, which files to edit, which commands to run.
## 💡 제안 ← Optional. Only include if a meaningfully better alternative exists. Omit otherwise.
## 제안 ← Optional. Only include if a meaningfully better alternative exists. Omit otherwise.
[FOLLOW-UP QUESTION RULES]
A follow-up question is a precision tool, not a ritual.
@@ -228,7 +228,10 @@ If neither condition is met, give a definitive answer and stop.
3. When the user says "분석해줘", "봐줘", "확인해줘", "리뷰해줘" with a path — that IS the confirmation. Access the path immediately and run the full analysis in one continuous response. Do not pause to ask "진행할까요?" or "시작할까요?".`;
export function getSystemPrompt(): string {
return BASE_SYSTEM_PROMPT;
const now = new Date();
const dateTimeStr = now.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long', hour: '2-digit', minute: '2-digit' });
const isoDate = now.toISOString().split('T')[0];
return `${BASE_SYSTEM_PROMPT}\n\n[CURRENT DATE/TIME]\nToday: ${isoDate} (${dateTimeStr})\nUse this date as the absolute reference for any date-related calculations (e.g., "this week", "today", "yesterday").`;
}
export const SYSTEM_PROMPT = BASE_SYSTEM_PROMPT;