feat: v2.12.0 - UI/UX Refinement (Model Sync & Premium Tooltips)

This commit is contained in:
Wonseok Jung
2026-04-30 00:19:06 +09:00
parent f8a57cfbb0
commit 326672cb93
25 changed files with 5606 additions and 363 deletions
+428 -179
View File
@@ -3,10 +3,8 @@ import * as path from 'path';
import * as fs from 'fs';
// axios removed
import {
getConfig,
_getBrainDir,
findBrainFiles,
EXCLUDED_DIRS,
getSystemPrompt,
shouldAutoPushBrain,
getSecondBrainRepo,
@@ -17,16 +15,53 @@ import {
resolveEngine,
summarizeText
} from './utils';
import { getConfig, EXCLUDED_DIRS } from './config';
import { validatePath, sanitizeCommand } from './security';
import { TransactionManager } from './core/transaction';
import { SessionManager } from './core/session';
import { PlannerAgent, ResearcherAgent, WriterAgent } from './agents/factory';
import { ErrorTranslator } from './core/errorHandler';
import {
AgentExecutionError,
FileSystemError,
APICommunicationError
} from './core/errors';
import { StatusBarManager, AgentStatus } from './core/statusBar';
import { lockManager } from './core/lock';
import { actionQueue } from './core/queue';
import { ConflictResolver } from './core/conflict';
export interface ChatMessage {
role: 'user' | 'assistant' | 'system';
content: string | any[];
content: string;
internal?: boolean;
rationale?: {
problem: string;
goal: string;
reasoning: string;
};
}
type HistoryChangeListener = (history: ChatMessage[]) => void | Promise<void>;
// --- Agent Roles & Workflows ---
export type AgentRole = 'planner' | 'researcher' | 'writer';
const AGENT_PROMPTS: Record<AgentRole, string> = {
planner: `You are the [Planner Agent]. Your goal is to analyze the user's request and create a detailed execution plan.
1. Breakdown the request into logical steps.
2. Identify key search keywords for the knowledge base.
3. Output your plan in a structured format using <plan> tags.`,
researcher: `You are the [Researcher Agent]. Your goal is to gather and analyze data based on the Planner's strategy.
1. Search the local knowledge base using the provided keywords.
2. Evaluate data reliability and extract relevant facts.
3. Output your findings using <research_results> tags.`,
writer: `You are the [Writer Agent]. Your goal is to synthesize all gathered information into a high-quality final report.
1. Use the data from the Researcher.
2. Follow the project's visual and tone-of-voice guidelines.
3. Deliver a logical, consistent, and polished response.`
};
export class AgentExecutor {
private chatHistory: ChatMessage[] = [];
private abortController: AbortController | null = null;
@@ -34,10 +69,44 @@ export class AgentExecutor {
private historyChangeListener: HistoryChangeListener | undefined;
private runSerial = 0;
private activeRunId = 0;
private transactionManager: TransactionManager;
private sessionManager: SessionManager;
private statusBarManager: StatusBarManager;
private currentTaskId: string = 'default_session';
constructor(
private context: vscode.ExtensionContext
) {}
) {
this.transactionManager = new TransactionManager();
this.sessionManager = new SessionManager(this.context);
this.statusBarManager = new StatusBarManager();
this.restoreLastSession();
}
private parseRationale(text: string) {
const match = text.match(/<rationale>([\s\S]*?)<\/rationale>/);
if (!match) return undefined;
const raw = match[1];
const problem = raw.match(/\[PROBLEM\]([\s\S]*?)(?=\[|$)/)?.[1]?.trim() || "";
const goal = raw.match(/\[GOAL\]([\s\S]*?)(?=\[|$)/)?.[1]?.trim() || "";
const reasoning = raw.match(/\[REASONING\]([\s\S]*?)(?=\[|$)/)?.[1]?.trim() || raw.trim();
return { problem, goal, reasoning };
}
private async restoreLastSession() {
try {
const lastSession = this.sessionManager.loadLastActiveSession();
if (lastSession) {
this.chatHistory = lastSession.history;
this.currentTaskId = lastSession.taskId;
logInfo(`Restored last session: ${this.currentTaskId}`);
}
} catch (error) {
logError('Failed to restore last session. Starting fresh.', error);
}
}
public setWebview(webview: vscode.Webview) {
this.webview = webview;
@@ -75,6 +144,20 @@ export class AgentExecutor {
this.emitHistoryChanged();
}
public async approveTransaction() {
if (!this.transactionManager.isActive()) return;
this.transactionManager.commit();
this.statusBarManager.updateStatus(AgentStatus.Success, 'Changes committed.');
this.webview?.postMessage({ type: 'streamChunk', value: '\n✅ **작업이 승인되어 반영되었습니다.**' });
}
public async rejectTransaction() {
if (!this.transactionManager.isActive()) return;
this.transactionManager.rollback();
this.statusBarManager.updateStatus(AgentStatus.Idle, 'Changes rolled back.');
this.webview?.postMessage({ type: 'streamChunk', value: '\n❌ **작업이 거부되어 모든 변경사항이 취소되었습니다.**' });
}
public async handlePrompt(
prompt: string | null,
modelName: string,
@@ -98,19 +181,35 @@ export class AgentExecutor {
temperature = 0.7,
systemPrompt = getSystemPrompt()
} = options;
const { ollamaUrl, defaultModel: configDefaultModel, timeout, multiAgentEnabled } = getConfig();
const runId = options.runId ?? (loopDepth === 0 ? ++this.runSerial : this.activeRunId);
// Decide whether to use Multi-Agent Workflow (for complex requests)
const isComplex = prompt && (prompt.length > 100 || /(분석|보고서|설계|기획|정리)/.test(prompt));
if (isComplex && loopDepth === 0 && multiAgentEnabled) {
return this.executeMultiAgentWorkflow(prompt!, modelName, options);
}
const hasVisionContent = Array.isArray(visionContent) ? visionContent.length > 0 : !!visionContent;
let requestTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
if (!this.webview) return;
try {
// 0. Safety Check: Rollback any dangling transaction from previous runs
if (this.transactionManager.isActive()) {
logInfo('Cleaning up dangling transaction from previous session.');
this.transactionManager.rollback();
}
this.statusBarManager.updateStatus(AgentStatus.Thinking);
if (loopDepth === 0) {
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
this.activeRunId = runId;
this.currentTaskId = `task_${Date.now()}`;
await this.context.workspaceState.update('lastActionStr', undefined);
}
@@ -170,7 +269,8 @@ export class AgentExecutor {
}
// 3. API Request Setup
const { ollamaUrl, defaultModel, timeout } = getConfig();
const { ollamaUrl, defaultModel: configDefaultModel, timeout } = getConfig();
const actualModel = modelName || configDefaultModel;
const reqMessages = [...this.chatHistory];
// Handle Vision Content Injection
@@ -184,7 +284,7 @@ export class AgentExecutor {
: [];
reqMessages[lastUserIdx] = {
role: 'user',
content: [...textParts, ...(visionContent || [])]
content: JSON.stringify([...textParts, ...(visionContent || [])])
};
}
}
@@ -208,12 +308,12 @@ export class AgentExecutor {
// 4. Call AI Engine
this.abortController = new AbortController();
requestTimeoutHandle = setTimeout(() => {
logError('AI request timed out.', { timeoutMs: timeout, model: modelName || defaultModel, loopDepth });
logError('AI request timed out.', { timeoutMs: timeout, model: actualModel, loopDepth });
this.abortController?.abort();
}, timeout);
const request = await this.createStreamingRequest({
baseUrl: ollamaUrl,
modelName: modelName || defaultModel,
modelName: actualModel,
reqMessages: messagesForRequest,
temperature
});
@@ -283,12 +383,15 @@ export class AgentExecutor {
}
// 5. Execute Actions
const assistantMessage: ChatMessage = { role: 'assistant', content: aiResponseText, internal: true };
const rationale = this.parseRationale(aiResponseText);
const assistantMessage: ChatMessage = { role: 'assistant', content: aiResponseText, internal: true, rationale };
this.chatHistory.push(assistantMessage);
this.statusBarManager.updateStatus(AgentStatus.Executing);
const report = await this.executeActions(aiResponseText, rootPath);
if (!aiResponseText.trim() && report.length === 0) {
this.chatHistory.pop();
logError('Model returned an empty response without actions.', { model: modelName || defaultModel, loopDepth });
logError('Model returned an empty response without actions.', { model: actualModel, loopDepth });
this.webview.postMessage({ type: 'error', value: 'AI model returned an empty response.' });
return;
}
@@ -332,9 +435,11 @@ export class AgentExecutor {
assistantMessage.internal = false;
this.emitHistoryChanged();
this.statusBarManager.updateStatus(AgentStatus.Success);
this.webview.postMessage({ type: 'streamChunk', value: aiResponseText });
} catch (error: any) {
this.statusBarManager.updateStatus(AgentStatus.Error, error.message);
logError('Agent prompt failed.', { error: error?.message || String(error), promptPreview: summarizeText(prompt || '', 200) });
if (!this.isStaleRun(runId)) {
this.webview.postMessage({ type: "error", value: `[Agent Error]: ${error.message}` });
@@ -383,16 +488,15 @@ export class AgentExecutor {
if (explicitPath) return explicitPath;
const namedProject = prompt.match(/([A-Za-z0-9._-]+)\s*(?:프로젝트|project)/i)?.[1];
if (!namedProject) return null;
if (!namedProject) return null; // No project keyword found, do not attempt to guess.
const searchRoots = [
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '',
'/Volumes/Data/project/Antigravity',
'/Volumes/Data/project',
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''
].filter(Boolean);
for (const root of searchRoots) {
const resolved = this.findDirectoryByName(root, namedProject, 4);
const resolved = this.findDirectoryByName(root, namedProject, 2); // Depth reduced to 2 for performance and accuracy.
if (resolved) return resolved;
}
@@ -424,10 +528,108 @@ export class AgentExecutor {
return null;
}
public async executeMultiAgentWorkflow(
prompt: string,
modelName: string,
options: any
) {
if (!this.webview) return;
this.statusBarManager.updateStatus(AgentStatus.Thinking, 'Multi-Agent Workflow Started');
this.webview.postMessage({ type: 'streamStart' });
try {
// Instantiate decoupled agents
const planner = new PlannerAgent(modelName);
const researcher = new ResearcherAgent(modelName);
const writer = new WriterAgent(modelName);
// Prepare Context
const activeBrain = getActiveBrainProfile();
const brainFiles = findBrainFiles(activeBrain.localBrainPath);
const brainContext = `Brain: ${activeBrain.name}, Files: ${brainFiles.length}`;
// --- Phase 1: Planner ---
this.webview.postMessage({ type: 'autoContinue', value: 'Planner: 전략 수립 중...' });
const plan = await planner.execute(prompt, brainContext);
this.webview.postMessage({ type: 'streamChunk', value: `\n\n### 📝 작업 계획 (Execution Plan)\n${plan}\n\n` });
// --- Phase 2: Researcher ---
this.webview.postMessage({ type: 'autoContinue', value: 'Researcher: 지식 검색 중...' });
const research = await researcher.execute(plan, brainContext);
this.webview.postMessage({ type: 'streamChunk', value: `\n\n### 🔍 분석 결과 (Research Findings)\n*(정보 수집 및 정제 완료)*\n\n` });
// --- Phase 3: Writer ---
this.webview.postMessage({ type: 'autoContinue', value: 'Writer: 보고서 작성 중...' });
const finalReport = await writer.execute(research, prompt);
this.webview.postMessage({ type: 'streamChunk', value: `\n\n--- \n\n${finalReport}` });
this.webview.postMessage({ type: 'streamEnd' });
this.chatHistory.push({ role: 'assistant', content: finalReport });
this.emitHistoryChanged();
this.statusBarManager.updateStatus(AgentStatus.Success, 'Workflow Complete');
this.webview.postMessage({ type: 'autoContinue', value: '✅ 분석이 완료되었습니다!' });
} catch (error: any) {
const friendly = ErrorTranslator.translate(error);
logError('Workflow failed', error);
// Format error using guideline-compliant UI (Red color scheme)
this.webview.postMessage({
type: 'error',
value: `### ${friendly.title}\n\n**상태:** ${friendly.message}\n\n**해결 방법:** ${friendly.action}`
});
this.statusBarManager.updateStatus(AgentStatus.Idle, 'Error occurred');
}
}
private async callAgent(role: AgentRole, prompt: string, modelName: string, options: any): Promise<string> {
const persona = AGENT_PROMPTS[role];
const { ollamaUrl, timeout } = getConfig();
const messages: ChatMessage[] = [
{ role: 'system', content: persona },
{ role: 'user', content: prompt }
];
const request = await this.createStreamingRequest({
baseUrl: ollamaUrl,
modelName: modelName,
reqMessages: messages,
temperature: 0.3 // Use lower temperature for planning and research
});
let responseText = '';
const reader = request.response.body?.getReader();
if (!reader) throw new Error("Agent response body is not readable.");
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed === 'data: [DONE]') continue;
try {
const json = JSON.parse(trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed);
const content = json.choices?.[0]?.delta?.content || json.message?.content || '';
responseText += content;
} catch (e) {}
}
}
return responseText;
}
private isProjectAnalysisRequest(normalized: string): boolean {
// Intentionally conservative: omit generic words like '조사', '설명', '파악'
// that appear in ordinary chat and would incorrectly bypass LM Studio
return /(분석|리뷰|설계|구조|어떤 프로그램|무슨 프로그램|제품 설명)/.test(normalized);
// Only trigger local analysis if the intent is strictly about a project-level overview.
// Avoid generic terms like 'analysis' or 'review' that are common in general coding chat.
const hasProjectKeyword = /(프로젝트|project|레포|repository)/.test(normalized);
const hasAnalysisIntent = /(전체 요약|제품 설명|어떤 프로그램|구조 파악|훑어보기)/.test(normalized);
return hasProjectKeyword && hasAnalysisIntent;
}
private buildProjectAnalysisReply(projectPath: string): string {
@@ -691,6 +893,13 @@ export class AgentExecutor {
private emitHistoryChanged() {
if (!this.historyChangeListener) return;
// Save session whenever history changes
this.sessionManager.saveSession(
this.currentTaskId,
this.chatHistory,
this.context.workspaceState.get<string>('lastActionStr')
);
Promise.resolve(this.historyChangeListener(this.getHistory())).catch((error: any) => {
logError('History change listener failed.', { error: error?.message || String(error) });
});
@@ -818,179 +1027,219 @@ export class AgentExecutor {
let brainModified = false;
let firstCreatedFile: string | undefined;
// Action 1: Create File
const createRegex = /<create_file\s+path=['"]?([^'"]+)['"]?>([\s\S]*?)<\/create_file>/gi;
let match;
while ((match = createRegex.exec(aiMessage)) !== null) {
const relPath = match[1].trim();
const content = match[2].trim();
try {
const absPath = validatePath(rootPath, relPath);
fs.mkdirSync(path.dirname(absPath), { recursive: true });
fs.writeFileSync(absPath, content, 'utf-8');
report.push(`✅ Created: ${relPath}`);
if (!firstCreatedFile) firstCreatedFile = absPath;
if (absPath.startsWith(_getBrainDir())) brainModified = true;
} catch (err: any) { report.push(`❌ Error Creating ${relPath}: ${err.message}`); }
}
try {
this.transactionManager.begin();
// Action 2: Edit File
const editRegex = /<edit_file\s+path=['"]?([^'"]+)['"]?>([\s\S]*?)<\/edit_file>/gi;
while ((match = editRegex.exec(aiMessage)) !== null) {
const relPath = match[1].trim();
const editContent = match[2].trim();
try {
const absPath = validatePath(rootPath, relPath);
if (fs.existsSync(absPath)) {
let currentContent = fs.readFileSync(absPath, 'utf-8');
const searchMatch = editContent.match(/<search>([\s\S]*?)<\/search>\s*<replace>([\s\S]*?)<\/replace>/i);
if (searchMatch) {
const searchStr = searchMatch[1];
const replaceStr = searchMatch[2];
if (currentContent.includes(searchStr)) {
currentContent = currentContent.replace(searchStr, replaceStr);
fs.writeFileSync(absPath, currentContent, 'utf-8');
report.push(`📝 Updated: ${relPath}`);
} else {
report.push(`⚠️ Search string not found in ${relPath}`);
}
} else {
fs.writeFileSync(absPath, editContent, 'utf-8');
report.push(`📝 Updated (Full): ${relPath}`);
}
// Action 1: Create File
const createRegex = /<create_file\s+path=['"]?([^'"]+)['"]?>([\s\S]*?)<\/create_file>/gi;
let match;
while ((match = createRegex.exec(aiMessage)) !== null) {
const relPath = match[1].trim();
const content = match[2].trim();
try {
const absPath = validatePath(rootPath, relPath);
await this.transactionManager.record(absPath);
fs.mkdirSync(path.dirname(absPath), { recursive: true });
fs.writeFileSync(absPath, content, 'utf-8');
report.push(`✅ Created: ${relPath}`);
if (!firstCreatedFile) firstCreatedFile = absPath;
if (absPath.startsWith(_getBrainDir())) brainModified = true;
} else {
report.push(`❌ File not found: ${relPath}`);
} catch (err: any) {
throw new FileSystemError(`Failed to create file ${relPath}: ${err.message}`, relPath, err);
}
} catch (err: any) { report.push(`❌ Error Editing ${relPath}: ${err.message}`); }
}
}
// Action 3: Delete File
const deleteRegex = /<delete_file\s+path=['"]?([^'"]+)['"]?\s*\/?>(?:<\/delete_file>)?/gi;
while ((match = deleteRegex.exec(aiMessage)) !== null) {
const relPath = match[1].trim();
try {
const absPath = validatePath(rootPath, relPath);
if (fs.existsSync(absPath)) {
fs.unlinkSync(absPath);
report.push(`🗑 Deleted: ${relPath}`);
} else {
report.push(`⚠️ Delete failed: ${relPath} not found`);
}
} catch (err: any) { report.push(`❌ Error Deleting ${relPath}: ${err.message}`); }
}
// Action 4: Read File
const readRegex = /<read_file\s+path=['"]?([^'"]+)['"]?\s*\/?>(?:<\/read_file>)?/gi;
while ((match = readRegex.exec(aiMessage)) !== null) {
const relPath = match[1].trim();
try {
const absPath = validatePath(rootPath, relPath);
if (fs.existsSync(absPath)) {
const content = fs.readFileSync(absPath, 'utf-8');
const preview = content.length > 8000 ? content.slice(0, 8000) + "\n... (truncated)" : content;
report.push(`📖 Read: ${relPath}`);
this.chatHistory.push({ role: 'system', content: `[Result of read_file ${relPath}]\n\`\`\`\n${preview}\n\`\`\``, internal: true });
} else {
report.push(`❌ Read failed: ${relPath} not found`);
}
} catch (err: any) { report.push(`❌ Error Reading ${relPath}: ${err.message}`); }
}
// Action 5: Run Command
const cmdRegex = /<run_command>([\s\S]*?)<\/run_command>/gi;
while ((match = cmdRegex.exec(aiMessage)) !== null) {
const cmd = match[1].trim();
try {
const safeCmd = sanitizeCommand(cmd);
const terminal = vscode.window.terminals.find(t => t.name === 'G1nation Terminal') || vscode.window.createTerminal({ name: 'G1nation Terminal', cwd: rootPath });
terminal.show();
terminal.sendText(safeCmd);
report.push(`🚀 Executed: ${safeCmd}`);
} catch (err: any) { report.push(`❌ Blocked: ${err.message}`); }
}
// Action 6: List Files
const listRegex = /<list_files\s+path=['"]?([^'"]+)['"]?\s*\/?>(?:<\/list_files>)?/gi;
while ((match = listRegex.exec(aiMessage)) !== null) {
const relPath = match[1].trim() || '.';
try {
const absPath = validatePath(rootPath, relPath);
if (fs.existsSync(absPath) && fs.statSync(absPath).isDirectory()) {
const entries = fs.readdirSync(absPath, { withFileTypes: true });
let listing = entries
.filter(e => !e.name.startsWith('.') && !EXCLUDED_DIRS.has(e.name))
.map(e => e.isDirectory() ? `${e.name}/` : e.name)
.join('\n');
if (listing.length > 5000) {
listing = listing.slice(0, 5000) + "\n... (truncated for context)";
// Action 2: Edit File
const editRegex = /<edit_file\s+path=['"]?([^'"]+)['"]?>([\s\S]*?)<\/edit_file>/gi;
while ((match = editRegex.exec(aiMessage)) !== null) {
const relPath = match[1].trim();
const editContent = match[2].trim();
try {
const absPath = validatePath(rootPath, relPath);
if (fs.existsSync(absPath)) {
await this.transactionManager.record(absPath);
let currentContent = fs.readFileSync(absPath, 'utf-8');
const searchMatch = editContent.match(/<search>([\s\S]*?)<\/search>\s*<replace>([\s\S]*?)<\/replace>/i);
if (searchMatch) {
const searchStr = searchMatch[1];
const replaceStr = searchMatch[2];
if (currentContent.includes(searchStr)) {
currentContent = currentContent.replace(searchStr, replaceStr);
fs.writeFileSync(absPath, currentContent, 'utf-8');
report.push(`📝 Updated: ${relPath}`);
} else {
report.push(`⚠️ Search string not found in ${relPath}`);
}
} else {
fs.writeFileSync(absPath, editContent, 'utf-8');
report.push(`📝 Updated (Full): ${relPath}`);
}
if (absPath.startsWith(_getBrainDir())) brainModified = true;
} else {
report.push(`❌ File not found: ${relPath}`);
}
report.push(`📂 Listed: ${relPath}`);
this.chatHistory.push({ role: 'system', content: `[Result of list_files ${relPath}]\n${listing}`, internal: true });
} catch (err: any) {
throw new FileSystemError(`Failed to edit file ${relPath}: ${err.message}`, relPath, err);
}
} catch (err: any) { report.push(`❌ Listing failed: ${err.message}`); }
}
}
// Action 7: Second Brain Knowledge (List/Read)
const listBrainRegex = /<list_brain\s*path=['"]?([^'"]*)['"]?\s*\/?>(?:<\/list_brain>)?/gi;
while ((match = listBrainRegex.exec(aiMessage)) !== null) {
const relPath = match[1].trim() || '.';
try {
const brainDir = _getBrainDir();
const absPath = path.join(brainDir, relPath);
if (fs.existsSync(absPath) && fs.statSync(absPath).isDirectory()) {
const entries = fs.readdirSync(absPath, { withFileTypes: true });
let listing = entries
.filter(e => !e.name.startsWith('.') && !EXCLUDED_DIRS.has(e.name))
.map(e => e.isDirectory() ? `${e.name}/` : e.name)
.join('\n');
if (listing.length > 5000) {
listing = listing.slice(0, 5000) + "\n... (truncated for context)";
// Action 3: Delete File
const deleteRegex = /<delete_file\s+path=['"]?([^'"]+)['"]?\s*\/?>(?:<\/delete_file>)?/gi;
while ((match = deleteRegex.exec(aiMessage)) !== null) {
const relPath = match[1].trim();
try {
const absPath = validatePath(rootPath, relPath);
if (fs.existsSync(absPath)) {
await this.transactionManager.record(absPath);
fs.unlinkSync(absPath);
report.push(`🗑 Deleted: ${relPath}`);
} else {
report.push(`⚠️ Delete failed: ${relPath} not found`);
}
} catch (err: any) {
throw new FileSystemError(`Failed to delete file ${relPath}: ${err.message}`, relPath, err);
}
}
// Action 4: Read File (Non-state-changing, no transaction record needed)
const readRegex = /<read_file\s+path=['"]?([^'"]+)['"]?\s*\/?>(?:<\/read_file>)?/gi;
while ((match = readRegex.exec(aiMessage)) !== null) {
const relPath = match[1].trim();
try {
const absPath = validatePath(rootPath, relPath);
if (fs.existsSync(absPath)) {
const content = fs.readFileSync(absPath, 'utf-8');
const preview = content.length > 8000 ? content.slice(0, 8000) + "\n... (truncated)" : content;
report.push(`📖 Read: ${relPath}`);
this.chatHistory.push({ role: 'system', content: `[Result of read_file ${relPath}]\n\`\`\`\n${preview}\n\`\`\``, internal: true });
} else {
report.push(`❌ Read failed: ${relPath} not found`);
}
} catch (err: any) { report.push(`❌ Error Reading ${relPath}: ${err.message}`); }
}
// Action 5: Run Command
const cmdRegex = /<run_command>([\s\S]*?)<\/run_command>/gi;
while ((match = cmdRegex.exec(aiMessage)) !== null) {
const cmd = match[1].trim();
try {
const safeCmd = sanitizeCommand(cmd);
const terminal = vscode.window.terminals.find(t => t.name === 'G1nation Terminal') || vscode.window.createTerminal({ name: 'G1nation Terminal', cwd: rootPath });
terminal.show();
terminal.sendText(safeCmd);
report.push(`🚀 Executed: ${safeCmd}`);
} catch (err: any) { report.push(`❌ Blocked: ${err.message}`); }
}
// Action 6: List Files
const listRegex = /<list_files\s+path=['"]?([^'"]+)['"]?\s*\/?>(?:<\/list_files>)?/gi;
while ((match = listRegex.exec(aiMessage)) !== null) {
const relPath = match[1].trim() || '.';
try {
const absPath = validatePath(rootPath, relPath);
if (fs.existsSync(absPath) && fs.statSync(absPath).isDirectory()) {
const entries = fs.readdirSync(absPath, { withFileTypes: true });
let listing = entries
.filter(e => !e.name.startsWith('.') && !EXCLUDED_DIRS.has(e.name))
.map(e => e.isDirectory() ? `${e.name}/` : e.name)
.join('\n');
if (listing.length > 5000) {
listing = listing.slice(0, 5000) + "\n... (truncated for context)";
}
report.push(`📂 Listed: ${relPath}`);
this.chatHistory.push({ role: 'system', content: `[Result of list_files ${relPath}]\n${listing}`, internal: true });
}
} catch (err: any) { report.push(`❌ Listing failed: ${err.message}`); }
}
// Action 7: Second Brain Knowledge (List/Read)
const listBrainRegex = /<list_brain\s*path=['"]?([^'"]*)['"]?\s*\/?>(?:<\/list_brain>)?/gi;
while ((match = listBrainRegex.exec(aiMessage)) !== null) {
const relPath = match[1].trim() || '.';
try {
const brainDir = _getBrainDir();
const absPath = path.join(brainDir, relPath);
if (fs.existsSync(absPath) && fs.statSync(absPath).isDirectory()) {
const entries = fs.readdirSync(absPath, { withFileTypes: true });
let listing = entries
.filter(e => !e.name.startsWith('.') && !EXCLUDED_DIRS.has(e.name))
.map(e => e.isDirectory() ? `${e.name}/` : e.name)
.join('\n');
if (listing.length > 5000) {
listing = listing.slice(0, 5000) + "\n... (truncated for context)";
}
report.push(`🧠 Brain Listed: ${relPath}`);
this.chatHistory.push({ role: 'system', content: `[Result of list_brain ${relPath}]\n${listing}`, internal: true });
} else {
report.push(`❌ Brain List failed: ${relPath} not found`);
}
} catch (err: any) { report.push(`❌ Error Listing Brain: ${err.message}`); }
}
const brainRegex = /<read_brain>([\s\S]*?)<\/read_brain>/gi;
while ((match = brainRegex.exec(aiMessage)) !== null) {
const fileName = match[1].trim();
try {
const brainDir = _getBrainDir();
const files = findBrainFiles(brainDir);
const targetFile = files.find((f: string) => path.basename(f) === fileName || f.endsWith(fileName));
report.push(`🧠 Brain Listed: ${relPath}`);
this.chatHistory.push({ role: 'system', content: `[Result of list_brain ${relPath}]\n${listing}`, internal: true });
} else {
report.push(`❌ Brain List failed: ${relPath} not found`);
}
} catch (err: any) { report.push(`❌ Error Listing Brain: ${err.message}`); }
}
if (targetFile && fs.existsSync(targetFile)) {
const content = fs.readFileSync(targetFile, 'utf-8');
report.push(`🧠 Brain Read: ${fileName}`);
this.chatHistory.push({ role: 'system', content: `[Result of read_brain ${fileName}]\n\`\`\`\n${content}\n\`\`\``, internal: true });
} else {
report.push(`❌ Brain Read failed: ${fileName} not found in Second Brain`);
}
} catch (err: any) { report.push(`❌ Error Reading Brain: ${err.message}`); }
}
const brainRegex = /<read_brain>([\s\S]*?)<\/read_brain>/gi;
while ((match = brainRegex.exec(aiMessage)) !== null) {
const fileName = match[1].trim();
try {
const brainDir = _getBrainDir();
const files = findBrainFiles(brainDir);
// Look for direct match or path match
const targetFile = files.find((f: string) => path.basename(f) === fileName || f.endsWith(fileName));
if (targetFile && fs.existsSync(targetFile)) {
const content = fs.readFileSync(targetFile, 'utf-8');
report.push(`🧠 Brain Read: ${fileName}`);
this.chatHistory.push({ role: 'system', content: `[Result of read_brain ${fileName}]\n\`\`\`\n${content}\n\`\`\``, internal: true });
} else {
report.push(`❌ Brain Read failed: ${fileName} not found in Second Brain`);
}
} catch (err: any) { report.push(`❌ Error Reading Brain: ${err.message}`); }
}
// Action 8: Read URL
const urlRegex = /<read_url>([\s\S]*?)<\/read_url>/gi;
while ((match = urlRegex.exec(aiMessage)) !== null) {
const url = match[1].trim();
try {
const res = await fetch(url, { signal: AbortSignal.timeout(10000) });
const text = await res.text();
const content = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
const preview = content.length > 5000 ? content.slice(0, 5000) + "\n... (truncated)" : content;
report.push(`🌐 Read URL: ${url}`);
this.chatHistory.push({ role: 'system', content: `[Result of read_url ${url}]\n${preview}`, internal: true });
} catch (err: any) { report.push(`❌ URL Read failed: ${err.message}`); }
}
// Action 8: Read URL (Simple implementation)
const urlRegex = /<read_url>([\s\S]*?)<\/read_url>/gi;
while ((match = urlRegex.exec(aiMessage)) !== null) {
const url = match[1].trim();
try {
const res = await fetch(url, { signal: AbortSignal.timeout(10000) });
const text = await res.text();
// Simple HTML to text-ish conversion
const content = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
const preview = content.length > 5000 ? content.slice(0, 5000) + "\n... (truncated)" : content;
report.push(`🌐 Read URL: ${url}`);
this.chatHistory.push({ role: 'system', content: `[Result of read_url ${url}]\n${preview}`, internal: true });
} catch (err: any) { report.push(`❌ URL Read failed: ${err.message}`); }
if (firstCreatedFile) {
vscode.window.showTextDocument(vscode.Uri.file(firstCreatedFile), { preview: false });
}
// Brain Sync Logic
if (brainModified && shouldAutoPushBrain() && getSecondBrainRepo()) {
this.syncBrain();
}
const config = getConfig();
if (config.dryRun) {
report.push(`\n⚠️ **Dry Run Mode Active**: 위 변경 사항을 확인하고 [승인] 또는 [롤백]을 선택해주세요.`);
this.webview?.postMessage({ type: 'requiresApproval' });
// Do NOT commit yet
} else {
this.transactionManager.commit();
}
} catch (error: any) {
this.transactionManager.rollback();
const g1Error = error instanceof AgentExecutionError ? error : new AgentExecutionError(error.message, error);
report.push(`🛑 Transaction Failed: ${g1Error.message}. All file changes rolled back.`);
logError('Action execution failed, rolled back.', g1Error);
// We return the report with the failure message instead of throwing
// so the agent can see the failure and decide what to do next
}
if (firstCreatedFile) {