chore: release v2.2.46 with critical bug fixes for AI communication and brain management
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# Patch Notes - v2.2.46
|
||||
|
||||
## 🚀 Key Improvements & Bug Fixes
|
||||
|
||||
### 1. AI Communication Protocol Fix
|
||||
- **Issue:** Previously, when a file was attached, the text prompt was completely overwritten by the `visionContent` structure, leading to empty messages being sent to LM Studio.
|
||||
- **Fix:** Merged the text prompt with the vision content array so that both text and file metadata are correctly transmitted.
|
||||
|
||||
### 2. Autonomous Loop Optimization
|
||||
- **Issue:** Broad keyword matching (e.g., "조사", "설명") triggered local file analysis tasks even for general questions, causing the agent to bypass the LLM and give incomplete answers.
|
||||
- **Fix:** Refined the `isProjectAnalysisRequest` patterns to be more conservative, ensuring generic conversational requests are always handled by the AI model.
|
||||
|
||||
### 3. Second Brain Profile Management
|
||||
- **Issue:** Adding new brain profiles was inconsistent because a virtual "default-brain" (injected in memory) was being saved into the permanent settings, causing profile list corruption.
|
||||
- **Fix:**
|
||||
- Decoupled runtime virtual profiles from persistence logic.
|
||||
- Implemented direct settings access for profile addition to avoid stale config caches.
|
||||
- Fixed UI sync issues immediately after adding a new brain folder.
|
||||
|
||||
### 4. Build & Reliability
|
||||
- Removed premature empty stream chunks that were causing UI flickering.
|
||||
- Verified build stability with `v2.2.46` VSIX package.
|
||||
|
||||
---
|
||||
*Date: 2026-04-25*
|
||||
*Version: 2.2.46*
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "g1nation",
|
||||
"version": "2.2.15",
|
||||
"version": "2.2.46",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "g1nation",
|
||||
"version": "2.2.15",
|
||||
"version": "2.2.46",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsdom": "^29.0.2"
|
||||
|
||||
+36
-1
@@ -2,7 +2,7 @@
|
||||
"name": "g1nation",
|
||||
"displayName": "G1nation",
|
||||
"description": "100% local AI coding agent for VS Code. Create files, edit code, run commands, and work offline with Ollama or LM Studio.",
|
||||
"version": "2.2.29",
|
||||
"version": "2.2.46",
|
||||
"publisher": "connectailab",
|
||||
"license": "MIT",
|
||||
"icon": "assets/icon.png",
|
||||
@@ -115,6 +115,41 @@
|
||||
"default": "",
|
||||
"description": "Folder path for your local Second Brain knowledge base. Leave empty to use the default folder."
|
||||
},
|
||||
"g1nation.brainProfiles": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Stable brain profile id."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name shown in the G1nation brain selector."
|
||||
},
|
||||
"localBrainPath": {
|
||||
"type": "string",
|
||||
"description": "Local folder path used as this brain's markdown knowledge base."
|
||||
},
|
||||
"secondBrainRepo": {
|
||||
"type": "string",
|
||||
"description": "Optional Git repository URL for this brain."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Short note shown under the active brain status."
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Multiple brain profiles. Each item supports id, name, localBrainPath, secondBrainRepo, and description."
|
||||
},
|
||||
"g1nation.activeBrainId": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Active brain profile id used for the current chat context."
|
||||
},
|
||||
"g1nation.secondBrainRepo": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
|
||||
+544
-50
@@ -7,10 +7,11 @@ import {
|
||||
_getBrainDir,
|
||||
findBrainFiles,
|
||||
EXCLUDED_DIRS,
|
||||
SYSTEM_PROMPT,
|
||||
getSystemPrompt,
|
||||
shouldAutoPushBrain,
|
||||
getSecondBrainRepo,
|
||||
buildApiUrl,
|
||||
getActiveBrainProfile,
|
||||
logError,
|
||||
logInfo,
|
||||
resolveEngine,
|
||||
@@ -24,10 +25,15 @@ export interface ChatMessage {
|
||||
internal?: boolean;
|
||||
}
|
||||
|
||||
type HistoryChangeListener = (history: ChatMessage[]) => void | Promise<void>;
|
||||
|
||||
export class AgentExecutor {
|
||||
private chatHistory: ChatMessage[] = [];
|
||||
private abortController: AbortController | null = null;
|
||||
private webview: vscode.Webview | undefined;
|
||||
private historyChangeListener: HistoryChangeListener | undefined;
|
||||
private runSerial = 0;
|
||||
private activeRunId = 0;
|
||||
|
||||
constructor(
|
||||
private context: vscode.ExtensionContext
|
||||
@@ -37,25 +43,38 @@ export class AgentExecutor {
|
||||
this.webview = webview;
|
||||
}
|
||||
|
||||
public setHistoryChangeListener(listener: HistoryChangeListener) {
|
||||
this.historyChangeListener = listener;
|
||||
}
|
||||
|
||||
public getHistory() {
|
||||
return this.chatHistory.filter(message => !message.internal);
|
||||
}
|
||||
|
||||
public setHistory(history: ChatMessage[]) {
|
||||
this.chatHistory = history;
|
||||
this.emitHistoryChanged();
|
||||
}
|
||||
|
||||
public clearHistory() {
|
||||
this.chatHistory = [];
|
||||
this.emitHistoryChanged();
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.activeRunId = ++this.runSerial;
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
this.abortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
public resetConversation() {
|
||||
this.stop();
|
||||
this.chatHistory = [];
|
||||
this.emitHistoryChanged();
|
||||
}
|
||||
|
||||
public async handlePrompt(
|
||||
prompt: string | null,
|
||||
modelName: string,
|
||||
@@ -65,7 +84,8 @@ export class AgentExecutor {
|
||||
loopDepth?: number,
|
||||
visionContent?: any[],
|
||||
temperature?: number,
|
||||
systemPrompt?: string
|
||||
systemPrompt?: string,
|
||||
runId?: number
|
||||
}
|
||||
) {
|
||||
const {
|
||||
@@ -74,22 +94,60 @@ export class AgentExecutor {
|
||||
loopDepth = 0,
|
||||
visionContent,
|
||||
temperature = 0.7,
|
||||
systemPrompt = SYSTEM_PROMPT
|
||||
systemPrompt = getSystemPrompt()
|
||||
} = options;
|
||||
const runId = options.runId ?? (loopDepth === 0 ? ++this.runSerial : this.activeRunId);
|
||||
const hasVisionContent = Array.isArray(visionContent) ? visionContent.length > 0 : !!visionContent;
|
||||
let requestTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
if (!this.webview) return;
|
||||
|
||||
try {
|
||||
if (loopDepth === 0) {
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
this.abortController = null;
|
||||
}
|
||||
this.activeRunId = runId;
|
||||
await this.context.workspaceState.update('lastActionStr', undefined);
|
||||
}
|
||||
|
||||
if (prompt !== null && loopDepth === 0 && !hasVisionContent) {
|
||||
const localReply = await this.tryHandleLocalTask(prompt);
|
||||
if (this.isStaleRun(runId)) return;
|
||||
if (localReply) {
|
||||
this.chatHistory.push({ role: 'user', content: prompt });
|
||||
this.emitHistoryChanged();
|
||||
this.webview.postMessage({ type: 'streamStart' });
|
||||
this.webview.postMessage({ type: 'streamChunk', value: localReply });
|
||||
this.webview.postMessage({ type: 'streamEnd' });
|
||||
this.chatHistory.push({ role: 'assistant', content: localReply });
|
||||
this.emitHistoryChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Prepare Context
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
const rootPath = workspaceFolders ? workspaceFolders[0].uri.fsPath : '';
|
||||
|
||||
let contextBlock = '';
|
||||
const config = getConfig();
|
||||
const activeBrain = getActiveBrainProfile();
|
||||
const brainFiles = findBrainFiles(activeBrain.localBrainPath);
|
||||
const brainPreview = brainFiles
|
||||
.slice(0, 30)
|
||||
.map(file => path.relative(activeBrain.localBrainPath, file))
|
||||
.join('\n');
|
||||
const brainContext = [
|
||||
`[ACTIVE SECOND BRAIN]`,
|
||||
`Use this Local Brain only when it is relevant to the user's current question.`,
|
||||
`Name: ${activeBrain.name}`,
|
||||
`Path: ${activeBrain.localBrainPath}`,
|
||||
`Knowledge files: ${brainFiles.length}`,
|
||||
activeBrain.description ? `Description: ${activeBrain.description}` : '',
|
||||
brainPreview ? `Available file examples:\n${brainPreview}` : 'Files: none found'
|
||||
].filter(Boolean).join('\n');
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor && editor.document.uri.scheme === 'file') {
|
||||
const text = editor.document.getText();
|
||||
@@ -103,7 +161,7 @@ export class AgentExecutor {
|
||||
if (prompt !== null) {
|
||||
if (loopDepth === 0) {
|
||||
this.chatHistory.push({ role: 'user', content: prompt });
|
||||
this.webview.postMessage({ type: 'streamChunk', value: '' }); // Trigger UI update if needed
|
||||
this.emitHistoryChanged();
|
||||
} else {
|
||||
this.chatHistory.push({ role: 'system', content: prompt, internal: true });
|
||||
}
|
||||
@@ -114,10 +172,18 @@ export class AgentExecutor {
|
||||
const reqMessages = [...this.chatHistory];
|
||||
|
||||
// Handle Vision Content Injection
|
||||
if (visionContent && reqMessages.length > 0) {
|
||||
// Merge text prompt with file content instead of replacing, so the user's message is never lost
|
||||
if (hasVisionContent && reqMessages.length > 0) {
|
||||
const lastUserIdx = reqMessages.map(m => m.role).lastIndexOf('user');
|
||||
if (lastUserIdx >= 0) {
|
||||
reqMessages[lastUserIdx] = { role: 'user', content: visionContent };
|
||||
const existingContent = reqMessages[lastUserIdx].content;
|
||||
const textParts: any[] = (typeof existingContent === 'string' && existingContent.trim())
|
||||
? [{ type: 'text', text: existingContent }]
|
||||
: [];
|
||||
reqMessages[lastUserIdx] = {
|
||||
role: 'user',
|
||||
content: [...textParts, ...(visionContent || [])]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +191,7 @@ export class AgentExecutor {
|
||||
const internetCtx = internetEnabled
|
||||
? `\n\n[CRITICAL: INTERNET ACCESS ENABLED]\nYou can use <read_url> to search. Current time: ${new Date().toLocaleString()}`
|
||||
: '';
|
||||
const fullSystemPrompt = `${systemPrompt}${internetCtx}\n\n[CONTEXT]\n${contextBlock}`;
|
||||
const fullSystemPrompt = `${systemPrompt}${internetCtx}\n\n[CONTEXT]\n${brainContext}\n${contextBlock}`;
|
||||
const messagesForRequest: ChatMessage[] = [
|
||||
{ role: 'system', content: fullSystemPrompt, internal: true },
|
||||
...reqMessages
|
||||
@@ -133,6 +199,10 @@ export class AgentExecutor {
|
||||
|
||||
// 4. Call AI Engine
|
||||
this.abortController = new AbortController();
|
||||
requestTimeoutHandle = setTimeout(() => {
|
||||
logError('AI request timed out.', { timeoutMs: timeout, model: modelName || defaultModel, loopDepth });
|
||||
this.abortController?.abort();
|
||||
}, timeout);
|
||||
const request = await this.createStreamingRequest({
|
||||
baseUrl: ollamaUrl,
|
||||
modelName: modelName || defaultModel,
|
||||
@@ -140,6 +210,7 @@ export class AgentExecutor {
|
||||
temperature
|
||||
});
|
||||
const { response, engine, apiUrl } = request;
|
||||
if (this.isStaleRun(runId)) return;
|
||||
|
||||
let aiResponseText = '';
|
||||
const reader = response.body?.getReader();
|
||||
@@ -153,6 +224,7 @@ export class AgentExecutor {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
if (this.isStaleRun(runId)) return;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
@@ -166,7 +238,6 @@ export class AgentExecutor {
|
||||
const token = engine === 'lmstudio' ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || '';
|
||||
if (token) {
|
||||
aiResponseText += token;
|
||||
this.webview?.postMessage({ type: 'streamChunk', value: token });
|
||||
}
|
||||
} catch (e: any) {
|
||||
logError('Failed to parse streaming chunk.', { engine, apiUrl, chunk: summarizeText(trimmed, 300), error: e?.message || String(e) });
|
||||
@@ -191,26 +262,41 @@ export class AgentExecutor {
|
||||
const token = engine === 'lmstudio' ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || '';
|
||||
if (token) {
|
||||
aiResponseText += token;
|
||||
this.webview?.postMessage({ type: 'streamChunk', value: token });
|
||||
}
|
||||
} catch (e: any) {
|
||||
logError('Failed to parse final streaming buffer.', { engine, apiUrl, buffer: summarizeText(buffer, 300), error: e?.message || String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
this.chatHistory.push({ role: 'assistant', content: aiResponseText });
|
||||
if (this.isStaleRun(runId)) return;
|
||||
if (requestTimeoutHandle) {
|
||||
clearTimeout(requestTimeoutHandle);
|
||||
requestTimeoutHandle = undefined;
|
||||
}
|
||||
|
||||
// 5. Execute Actions
|
||||
const assistantMessage: ChatMessage = { role: 'assistant', content: aiResponseText, internal: true };
|
||||
this.chatHistory.push(assistantMessage);
|
||||
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 });
|
||||
this.webview.postMessage({ type: 'error', value: 'AI model returned an empty response.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (report.length === 0 && loopDepth === 0 && this.isUnproductiveWaitingReply(aiResponseText)) {
|
||||
assistantMessage.internal = false;
|
||||
const correctedReply = this.buildUnproductiveReplyCorrection(prompt || '');
|
||||
assistantMessage.content = correctedReply;
|
||||
this.emitHistoryChanged();
|
||||
this.webview.postMessage({ type: 'streamChunk', value: correctedReply });
|
||||
return;
|
||||
}
|
||||
|
||||
if (report.length > 0) {
|
||||
const reportMsg = `\n\n> ⚙️ **System Action Report** (${loopDepth + 1}/${config.maxAutoSteps})\n> ${report.join("\n> ")}\n\n`;
|
||||
this.webview.postMessage({ type: 'streamChunk', value: reportMsg });
|
||||
this.emitHistoryChanged();
|
||||
logInfo('Agent actions executed.', { loopDepth: loopDepth + 1, report });
|
||||
|
||||
// Continue loop if needed
|
||||
if (loopDepth < config.maxAutoSteps) {
|
||||
@@ -226,24 +312,370 @@ export class AgentExecutor {
|
||||
logInfo('Autonomous loop continuing after actions.', { loopDepth: loopDepth + 1, actions: report });
|
||||
|
||||
// Explicitly tell the AI to look at the results and continue
|
||||
const continuationPrompt = "I have executed your actions. Above is the result. Please analyze it and provide the next step or the final answer.";
|
||||
const continuationPrompt = "The requested local action has been executed. Use the action result messages already in the conversation to answer the user's original request directly, in the user's language. Do not say you are waiting for the next instruction.";
|
||||
|
||||
this.webview.postMessage({ type: 'autoContinue', value: `Thinking... (${loopDepth + 1}/${config.maxAutoSteps})` });
|
||||
this.webview.postMessage({ type: 'autoContinue', value: `자료를 확인하고 답변을 정리하는 중입니다... (${loopDepth + 1}/${config.maxAutoSteps})` });
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
await this.handlePrompt(continuationPrompt, modelName, { ...options, loopDepth: loopDepth + 1 });
|
||||
if (this.isStaleRun(runId)) return;
|
||||
await this.handlePrompt(continuationPrompt, modelName, { ...options, loopDepth: loopDepth + 1, runId });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
assistantMessage.internal = false;
|
||||
this.emitHistoryChanged();
|
||||
this.webview.postMessage({ type: 'streamChunk', value: aiResponseText });
|
||||
|
||||
} catch (error: any) {
|
||||
logError('Agent prompt failed.', { error: error?.message || String(error), promptPreview: summarizeText(prompt || '', 200) });
|
||||
this.webview.postMessage({ type: "error", value: `[Agent Error]: ${error.message}` });
|
||||
if (!this.isStaleRun(runId)) {
|
||||
this.webview.postMessage({ type: "error", value: `[Agent Error]: ${error.message}` });
|
||||
}
|
||||
} finally {
|
||||
if (loopDepth === 0) {
|
||||
if (requestTimeoutHandle) {
|
||||
clearTimeout(requestTimeoutHandle);
|
||||
}
|
||||
if (loopDepth === 0 && !this.isStaleRun(runId)) {
|
||||
this.webview.postMessage({ type: 'streamEnd' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async tryHandleLocalTask(prompt: string): Promise<string | null> {
|
||||
const normalized = prompt.trim().toLowerCase();
|
||||
if (!normalized) return null;
|
||||
|
||||
const projectPath = this.resolveProjectReference(prompt);
|
||||
if (projectPath && this.isProjectAnalysisRequest(normalized)) {
|
||||
return this.buildProjectAnalysisReply(projectPath);
|
||||
}
|
||||
|
||||
if (this.isBrainOverviewRequest(normalized)) {
|
||||
return this.buildBrainOverviewReply();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private extractExistingProjectPath(prompt: string): string | null {
|
||||
const pathMatches = prompt.match(/(?:~|\/)[^\s`'"]+/g) || [];
|
||||
for (const rawPath of pathMatches) {
|
||||
const expandedPath = rawPath.startsWith('~/')
|
||||
? path.join(require('os').homedir(), rawPath.slice(2))
|
||||
: rawPath;
|
||||
if (fs.existsSync(expandedPath)) {
|
||||
return expandedPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private resolveProjectReference(prompt: string): string | null {
|
||||
const explicitPath = this.extractExistingProjectPath(prompt);
|
||||
if (explicitPath) return explicitPath;
|
||||
|
||||
const namedProject = prompt.match(/([A-Za-z0-9._-]+)\s*(?:프로젝트|project)/i)?.[1];
|
||||
if (!namedProject) return null;
|
||||
|
||||
const searchRoots = [
|
||||
'/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);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private findDirectoryByName(root: string, targetName: string, maxDepth: number): string | null {
|
||||
if (!root || maxDepth < 0 || !fs.existsSync(root)) return null;
|
||||
const normalizedTarget = targetName.toLowerCase();
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(root, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name));
|
||||
|
||||
const exact = entries.find(entry => entry.name.toLowerCase() === normalizedTarget);
|
||||
if (exact) return path.join(root, exact.name);
|
||||
|
||||
const partial = entries.find(entry => entry.name.toLowerCase().includes(normalizedTarget));
|
||||
if (partial) return path.join(root, partial.name);
|
||||
|
||||
for (const entry of entries) {
|
||||
const found = this.findDirectoryByName(path.join(root, entry.name), targetName, maxDepth - 1);
|
||||
if (found) return found;
|
||||
}
|
||||
} catch (error: any) {
|
||||
logError('Project name search failed.', { root, targetName, error: error?.message || String(error) });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private buildProjectAnalysisReply(projectPath: string): string {
|
||||
const stat = fs.statSync(projectPath);
|
||||
if (!stat.isDirectory()) {
|
||||
const content = fs.readFileSync(projectPath, 'utf-8');
|
||||
return [
|
||||
`요청하신 파일을 실제로 읽었습니다: \`${projectPath}\``,
|
||||
'',
|
||||
`파일 크기: ${content.length.toLocaleString()}자`,
|
||||
'',
|
||||
'첫 부분 요약:',
|
||||
'```',
|
||||
content.slice(0, 1800),
|
||||
content.length > 1800 ? '\n... (truncated)' : '',
|
||||
'```'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const packagePath = path.join(projectPath, 'package.json');
|
||||
const readmePath = this.findFirstExisting(projectPath, ['README.md', 'readme.md', 'README.MD']);
|
||||
const files = this.collectProjectFiles(projectPath, 600);
|
||||
const sourceFiles = files.filter(file => /\/src\/|\/app\/|\/pages\/|\/components\/|\/lib\//.test(file));
|
||||
const testFiles = files.filter(file => /\.(test|spec)\.[jt]sx?$|\/__tests__\//.test(file));
|
||||
const configFiles = files.filter(file => /(^|\/)(package\.json|tsconfig\.json|vite\.config\.|next\.config\.|tailwind\.config\.|eslint\.config\.|\.eslintrc|dockerfile|docker-compose|README\.md)/i.test(file));
|
||||
|
||||
let pkg: any = null;
|
||||
if (fs.existsSync(packagePath)) {
|
||||
try {
|
||||
pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
|
||||
} catch (error: any) {
|
||||
logError('Failed to parse package.json during local project analysis.', { projectPath, error: error?.message || String(error) });
|
||||
}
|
||||
}
|
||||
|
||||
const readmeText = readmePath ? fs.readFileSync(readmePath, 'utf-8') : '';
|
||||
const topDirs = this.summarizeTopDirectories(projectPath);
|
||||
const stack = this.inferStack(pkg, files);
|
||||
const entryPoints = this.inferEntryPoints(pkg, files);
|
||||
const readmeSummary = this.summarizeReadme(readmeText);
|
||||
const reviewFindings = this.buildProjectReviewFindings({ pkg, files, sourceFiles, testFiles, readmeText });
|
||||
|
||||
return [
|
||||
`제가 실제로 \`${projectPath}\` 폴더를 읽고 1차 분석했습니다.`,
|
||||
'',
|
||||
'**제품 설명**',
|
||||
pkg?.name
|
||||
? `- 이 프로젝트는 \`${pkg.name}\`${pkg.description ? ` (${pkg.description})` : ''}로 식별됩니다.`
|
||||
: '- `package.json` 기준의 제품명은 확인되지 않았습니다.',
|
||||
readmeSummary ? `- README 기준 핵심 설명: ${readmeSummary}` : '- README 기반 제품 설명은 부족합니다. 제품 소개 문서를 보강하는 편이 좋습니다.',
|
||||
stack.length ? `- 감지된 기술 스택: ${stack.join(', ')}` : '- 기술 스택은 파일 구조만으로는 명확하지 않습니다.',
|
||||
'',
|
||||
'**설계 구조**',
|
||||
topDirs.length ? topDirs.map(item => `- ${item}`).join('\n') : '- 상위 디렉터리 구조가 단순하거나 비어 있습니다.',
|
||||
entryPoints.length ? `- 주요 진입점 후보: ${entryPoints.join(', ')}` : '- 명확한 앱 진입점은 아직 식별되지 않았습니다.',
|
||||
configFiles.length ? `- 주요 설정 파일: ${configFiles.slice(0, 12).join(', ')}` : '- 주요 설정 파일이 많지 않습니다.',
|
||||
'',
|
||||
'**코드 리뷰 관점의 1차 소견**',
|
||||
reviewFindings.join('\n'),
|
||||
'',
|
||||
'**다음에 더 깊게 볼 부분**',
|
||||
'- 실제 비즈니스 플로우는 `src`, `app`, `lib`, `components` 내부 핵심 파일을 2차로 읽어야 정확히 판단할 수 있습니다.',
|
||||
'- 지금 단계에서는 “아무것도 안 하고 말만 하는” 답변이 아니라, 실제 파일 시스템을 기준으로 프로젝트 형태를 먼저 파악한 결과입니다.',
|
||||
'- 원하시면 다음 턴에서 제가 핵심 소스 파일을 더 읽어 아키텍처 다이어그램 수준으로 이어서 정리하겠습니다.'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
private findFirstExisting(basePath: string, names: string[]): string | null {
|
||||
for (const name of names) {
|
||||
const candidate = path.join(basePath, name);
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private collectProjectFiles(dir: string, limit: number, baseDir: string = dir): string[] {
|
||||
if (limit <= 0 || !fs.existsSync(dir)) return [];
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
.filter(entry => !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const results: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (results.length >= limit) break;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...this.collectProjectFiles(fullPath, limit - results.length, baseDir));
|
||||
} else {
|
||||
results.push(path.relative(baseDir, fullPath));
|
||||
}
|
||||
}
|
||||
|
||||
return results.slice(0, limit);
|
||||
}
|
||||
|
||||
private summarizeTopDirectories(projectPath: string): string[] {
|
||||
return fs.readdirSync(projectPath, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name))
|
||||
.slice(0, 12)
|
||||
.map(entry => {
|
||||
const count = this.collectProjectFiles(path.join(projectPath, entry.name), 200, projectPath).length;
|
||||
return `\`${entry.name}/\`: 약 ${count}개 파일`;
|
||||
});
|
||||
}
|
||||
|
||||
private inferStack(pkg: any, files: string[]): string[] {
|
||||
const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
|
||||
const stack = new Set<string>();
|
||||
if (deps.next || files.some(file => file.startsWith('app/') || file.startsWith('pages/'))) stack.add('Next.js');
|
||||
if (deps.react || files.some(file => /\.(jsx|tsx)$/.test(file))) stack.add('React');
|
||||
if (deps.typescript || files.some(file => file.endsWith('.ts') || file.endsWith('.tsx'))) stack.add('TypeScript');
|
||||
if (deps.vite || files.some(file => file.startsWith('vite.config.'))) stack.add('Vite');
|
||||
if (deps.tailwindcss || files.some(file => file.startsWith('tailwind.config.'))) stack.add('Tailwind CSS');
|
||||
if (deps.express) stack.add('Express');
|
||||
if (deps.electron) stack.add('Electron');
|
||||
if (deps.prisma || files.some(file => file.startsWith('prisma/'))) stack.add('Prisma');
|
||||
if (files.some(file => file.includes('docker-compose') || file.toLowerCase() === 'dockerfile')) stack.add('Docker');
|
||||
return Array.from(stack);
|
||||
}
|
||||
|
||||
private inferEntryPoints(pkg: any, files: string[]): string[] {
|
||||
const candidates = [
|
||||
pkg?.main,
|
||||
'src/main.ts',
|
||||
'src/index.ts',
|
||||
'src/App.tsx',
|
||||
'src/app.ts',
|
||||
'app/page.tsx',
|
||||
'pages/index.tsx',
|
||||
'server/index.ts',
|
||||
'index.js'
|
||||
].filter(Boolean) as string[];
|
||||
return candidates.filter((candidate, index) => candidates.indexOf(candidate) === index && files.includes(candidate));
|
||||
}
|
||||
|
||||
private summarizeReadme(readmeText: string): string {
|
||||
if (!readmeText.trim()) return '';
|
||||
const usefulLines = readmeText
|
||||
.split(/\r?\n/)
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#') && !line.startsWith('![') && !line.startsWith('<'))
|
||||
.slice(0, 3)
|
||||
.join(' ');
|
||||
return summarizeText(usefulLines, 260);
|
||||
}
|
||||
|
||||
private buildProjectReviewFindings(params: {
|
||||
pkg: any;
|
||||
files: string[];
|
||||
sourceFiles: string[];
|
||||
testFiles: string[];
|
||||
readmeText: string;
|
||||
}): string[] {
|
||||
const findings: string[] = [];
|
||||
if (!params.readmeText.trim()) {
|
||||
findings.push('- README가 없거나 비어 있어, 사용자가 제품 목적/실행 방법을 이해하기 어렵습니다.');
|
||||
}
|
||||
if (params.testFiles.length === 0) {
|
||||
findings.push('- 테스트 파일이 감지되지 않았습니다. 핵심 비즈니스 로직이나 API 레이어부터 최소 회귀 테스트를 추가하는 것이 좋습니다.');
|
||||
}
|
||||
if (!params.pkg?.scripts || Object.keys(params.pkg.scripts).length === 0) {
|
||||
findings.push('- `package.json` scripts가 없거나 부족합니다. `dev`, `build`, `test`, `lint` 같은 표준 명령이 있으면 제품 운용성이 좋아집니다.');
|
||||
} else {
|
||||
const scripts = Object.keys(params.pkg.scripts);
|
||||
const missing = ['build', 'test', 'lint'].filter(script => !scripts.includes(script));
|
||||
if (missing.length) findings.push(`- 누락된 표준 스크립트 후보: ${missing.map(script => `\`${script}\``).join(', ')}.`);
|
||||
}
|
||||
if (params.sourceFiles.length > 80 && params.testFiles.length < 3) {
|
||||
findings.push('- 소스 파일 규모에 비해 테스트 밀도가 낮아 보입니다. 화면/상태/API 경계를 기준으로 테스트 전략을 분리하는 편이 안전합니다.');
|
||||
}
|
||||
if (!params.files.some(file => /env\.example|\.env\.example|\.env\.sample/i.test(file))) {
|
||||
findings.push('- 환경 변수 예시 파일이 보이지 않습니다. 로컬 실행과 배포 재현성을 위해 `.env.example`을 권장합니다.');
|
||||
}
|
||||
if (findings.length === 0) {
|
||||
findings.push('- 1차 구조상 큰 결함은 바로 보이지 않습니다. 다음 단계에서는 핵심 소스 파일을 읽어 데이터 흐름, 에러 처리, 상태 관리 경계를 봐야 합니다.');
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
private isUnproductiveWaitingReply(reply: string): boolean {
|
||||
const normalized = reply.replace(/\s+/g, ' ').trim();
|
||||
return /(준비되었습니다|다음 지시|말씀해 주세요|명령을 기다|작업을 도와드릴까요)/.test(normalized)
|
||||
&& !/<(list_files|read_file|list_brain|read_brain|run_command|edit_file|create_file)/i.test(reply);
|
||||
}
|
||||
|
||||
private buildUnproductiveReplyCorrection(prompt: string): string {
|
||||
const projectPath = this.resolveProjectReference(prompt);
|
||||
if (projectPath && this.isProjectAnalysisRequest(prompt.toLowerCase())) {
|
||||
return this.buildProjectAnalysisReply(projectPath);
|
||||
}
|
||||
|
||||
return '방금 답변은 잘못된 응답입니다. 사용자의 말은 “다음 지시를 달라”가 아니라 지금 바로 처리해야 하는 작업 지시입니다. 제가 먼저 관련 자료를 확인하고, 확인한 내용 기준으로 답변하겠습니다. 가능하면 프로젝트명 대신 정확한 폴더 경로를 함께 주시면 더 안정적으로 분석할 수 있습니다.';
|
||||
}
|
||||
|
||||
private isBrainOverviewRequest(normalized: string): boolean {
|
||||
const mentionsBrain = /(제2뇌|second brain|local brain|브레인|brain)/.test(normalized);
|
||||
const asksOverview = /(어떠|뭐가|무엇|내용|정보|구성|준비|분석|정리|파악)/.test(normalized);
|
||||
return mentionsBrain && asksOverview;
|
||||
}
|
||||
|
||||
private buildBrainOverviewReply(): string {
|
||||
const activeBrain = getActiveBrainProfile();
|
||||
const brainDir = activeBrain.localBrainPath;
|
||||
const files = findBrainFiles(brainDir);
|
||||
|
||||
if (!fs.existsSync(brainDir)) {
|
||||
return `현재 선택된 제2뇌는 **${activeBrain.name}**인데, 폴더가 아직 존재하지 않습니다.\n\n경로: \`${brainDir}\`\n\n상단의 Brain 설정에서 실제 지식 폴더를 연결하면 제가 그 자료를 먼저 참고해서 답변하겠습니다.`;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(brainDir, { withFileTypes: true })
|
||||
.filter((entry) => !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name));
|
||||
|
||||
const directorySummaries = entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.slice(0, 12)
|
||||
.map((entry) => {
|
||||
const dirPath = path.join(brainDir, entry.name);
|
||||
const count = findBrainFiles(dirPath).length;
|
||||
return `- ${entry.name}/: ${count}개 문서`;
|
||||
});
|
||||
|
||||
const fileSummaries = entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
||||
.slice(0, 8)
|
||||
.map((entry) => `- ${entry.name}`);
|
||||
|
||||
const sampleFiles = files
|
||||
.slice(0, 10)
|
||||
.map((file) => `- ${path.relative(brainDir, file)}`);
|
||||
|
||||
const sections = [
|
||||
`현재 연결된 제2뇌는 **${activeBrain.name}**입니다.`,
|
||||
`경로: \`${brainDir}\``,
|
||||
`총 Markdown 지식 문서: **${files.length}개**`,
|
||||
activeBrain.description ? `설명: ${activeBrain.description}` : '',
|
||||
directorySummaries.length ? `\n상위 폴더 구조는 이렇게 보입니다.\n${directorySummaries.join('\n')}` : '',
|
||||
fileSummaries.length ? `\n루트에 있는 주요 문서는 이런 것들이 있습니다.\n${fileSummaries.join('\n')}` : '',
|
||||
sampleFiles.length ? `\n제가 우선 참고할 수 있는 샘플 문서는 다음과 같습니다.\n${sampleFiles.join('\n')}` : '',
|
||||
`\n이 자료는 충분히 도움이 됩니다. 앞으로 질문을 받으면 먼저 이 제2뇌에서 관련 기준이나 맥락을 찾고, 부족할 때만 제 일반 지식으로 보완하겠습니다.`
|
||||
].filter(Boolean);
|
||||
|
||||
return sections.join('\n');
|
||||
}
|
||||
|
||||
private isStaleRun(runId: number): boolean {
|
||||
return runId !== this.activeRunId;
|
||||
}
|
||||
|
||||
private emitHistoryChanged() {
|
||||
if (!this.historyChangeListener) return;
|
||||
|
||||
Promise.resolve(this.historyChangeListener(this.getHistory())).catch((error: any) => {
|
||||
logError('History change listener failed.', { error: error?.message || String(error) });
|
||||
});
|
||||
}
|
||||
|
||||
private async createStreamingRequest(params: {
|
||||
baseUrl: string;
|
||||
modelName: string;
|
||||
@@ -257,48 +689,110 @@ export class AgentExecutor {
|
||||
|
||||
for (const engine of engines) {
|
||||
const apiUrl = buildApiUrl(baseUrl, engine, 'chat');
|
||||
const streamBody = {
|
||||
model: modelName,
|
||||
messages: reqMessages,
|
||||
stream: true,
|
||||
...(engine === 'lmstudio'
|
||||
? { max_tokens: 4096, temperature }
|
||||
: { options: { num_ctx: 32768, num_predict: 4096, temperature } }),
|
||||
};
|
||||
const messageVariants = this.buildEngineMessageVariants(reqMessages, engine);
|
||||
const modelCandidates = this.buildModelCandidates(modelName, engine);
|
||||
|
||||
try {
|
||||
logInfo('AI streaming request started.', { engine, apiUrl, model: modelName, messageCount: reqMessages.length });
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
},
|
||||
body: JSON.stringify(streamBody),
|
||||
signal: this.abortController?.signal,
|
||||
keepalive: true
|
||||
});
|
||||
for (const candidateModel of modelCandidates) {
|
||||
for (const variant of messageVariants) {
|
||||
const streamBody = {
|
||||
model: candidateModel,
|
||||
messages: variant.messages,
|
||||
stream: true,
|
||||
...(engine === 'lmstudio'
|
||||
? { max_tokens: 4096, temperature }
|
||||
: { options: { num_ctx: 32768, num_predict: 4096, temperature } }),
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
lastError = new Error(`AI Engine error (${engine}): ${response.status} - ${summarizeText(errText, 300)}`);
|
||||
logError('AI streaming request returned non-OK status.', { engine, apiUrl, status: response.status, body: summarizeText(errText, 500) });
|
||||
continue;
|
||||
try {
|
||||
logInfo('AI streaming request started.', {
|
||||
engine,
|
||||
apiUrl,
|
||||
model: candidateModel,
|
||||
variant: variant.name,
|
||||
messageCount: variant.messages.length,
|
||||
roles: variant.messages.map(message => message.role),
|
||||
firstUserPreview: summarizeText(String(variant.messages.find(message => message.role === 'user')?.content || ''), 300)
|
||||
});
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
},
|
||||
body: JSON.stringify(streamBody),
|
||||
signal: this.abortController?.signal,
|
||||
keepalive: true
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
lastError = new Error(`AI Engine error (${engine}/${variant.name}): ${response.status} - ${summarizeText(errText, 300)}`);
|
||||
logError('AI streaming request returned non-OK status.', { engine, variant: variant.name, apiUrl, status: response.status, body: summarizeText(errText, 500) });
|
||||
continue;
|
||||
}
|
||||
|
||||
logInfo('AI streaming request connected.', { engine, variant: variant.name, apiUrl });
|
||||
return { response, engine, apiUrl };
|
||||
} catch (error: any) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
logError('AI streaming request failed.', { engine, variant: variant.name, apiUrl, model: candidateModel, error: lastError.message });
|
||||
}
|
||||
|
||||
logInfo('AI streaming request connected.', { engine, apiUrl });
|
||||
return { response, engine, apiUrl };
|
||||
} catch (error: any) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
logError('AI streaming request failed.', { engine, apiUrl, error: lastError.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('Unable to connect to AI engine.');
|
||||
}
|
||||
|
||||
private normalizeMessages(messages: ChatMessage[]) {
|
||||
return messages.map((message) => {
|
||||
const normalizedContent = typeof message.content === 'string'
|
||||
? message.content
|
||||
: JSON.stringify(message.content);
|
||||
|
||||
return {
|
||||
role: message.role,
|
||||
content: normalizedContent
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private buildEngineMessageVariants(messages: ChatMessage[], engine: 'lmstudio' | 'ollama') {
|
||||
const normalized = this.normalizeMessages(messages);
|
||||
if (engine !== 'lmstudio') {
|
||||
return [{ name: 'native', messages: normalized }];
|
||||
}
|
||||
|
||||
const flattened = normalized.map((message) => {
|
||||
if (message.role === 'system') {
|
||||
return {
|
||||
role: 'user' as const,
|
||||
content: `[System Instruction - do not answer this message]\n${message.content}`
|
||||
};
|
||||
}
|
||||
|
||||
return message;
|
||||
});
|
||||
|
||||
return [
|
||||
{ name: 'native-system', messages: normalized },
|
||||
{ name: 'flattened-system-fallback', messages: flattened }
|
||||
];
|
||||
}
|
||||
|
||||
private buildModelCandidates(modelName: string, engine: 'lmstudio' | 'ollama'): string[] {
|
||||
const candidates = [modelName];
|
||||
if (engine === 'lmstudio') {
|
||||
const baseModel = modelName.replace(/:\d+$/, '');
|
||||
if (baseModel && baseModel !== modelName) {
|
||||
candidates.push(baseModel);
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private async executeActions(aiMessage: string, rootPath: string): Promise<string[]> {
|
||||
const report: string[] = [];
|
||||
let brainModified = false;
|
||||
|
||||
+449
-52
@@ -1,31 +1,57 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
getConfig,
|
||||
_getBrainDir,
|
||||
findBrainFiles,
|
||||
buildApiUrl,
|
||||
getActiveBrainProfile,
|
||||
getBrainProfiles,
|
||||
logError,
|
||||
logInfo,
|
||||
resolveEngine,
|
||||
summarizeText
|
||||
} from './utils';
|
||||
import { AgentExecutor } from './agent';
|
||||
import { AgentExecutor, ChatMessage } from './agent';
|
||||
import { BridgeInterface } from './bridge';
|
||||
|
||||
interface LastVisibleChatSnapshot {
|
||||
history: ChatMessage[];
|
||||
brainProfileId: string;
|
||||
sessionId: string | null;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface ChatSession {
|
||||
id: string;
|
||||
title: string;
|
||||
timestamp: number;
|
||||
history: ChatMessage[];
|
||||
brainProfileId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sidebar UI Provider implementing BridgeInterface for BridgeServer
|
||||
*/
|
||||
export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeInterface {
|
||||
public static readonly viewType = 'g1nation-v2-view';
|
||||
private static readonly activeSessionStateKey = 'g1nation.activeSessionId';
|
||||
private static readonly lastVisibleChatStateKey = 'g1nation.lastVisibleChat';
|
||||
private static readonly blankChatStateKey = 'g1nation.blankChatActive';
|
||||
private _view?: vscode.WebviewView;
|
||||
public brainEnabled = true;
|
||||
private _currentSessionBrainId: string | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly _extensionUri: vscode.Uri,
|
||||
private readonly _context: vscode.ExtensionContext,
|
||||
private readonly _agent: AgentExecutor
|
||||
) {}
|
||||
) {
|
||||
this._agent.setHistoryChangeListener((history) => {
|
||||
void this._persistLastVisibleChat(history);
|
||||
});
|
||||
}
|
||||
|
||||
public resolveWebviewView(
|
||||
webviewView: vscode.WebviewView,
|
||||
@@ -42,33 +68,36 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
webviewView.webview.html = this._getHtml(webviewView.webview);
|
||||
this._agent.setWebview(webviewView.webview);
|
||||
|
||||
// Re-hydrate existing history
|
||||
const currentHistory = this._agent.getHistory();
|
||||
if (currentHistory.length > 0) {
|
||||
setTimeout(() => {
|
||||
webviewView.webview.postMessage({ type: 'restoreHistory', value: currentHistory });
|
||||
}, 500);
|
||||
}
|
||||
void this._restoreActiveSessionIntoView();
|
||||
|
||||
webviewView.webview.onDidReceiveMessage(async (data) => {
|
||||
switch (data.type) {
|
||||
case 'prompt':
|
||||
case 'promptWithFile':
|
||||
await this._context.globalState.update(SidebarChatProvider.blankChatStateKey, false);
|
||||
await this._handlePrompt(data);
|
||||
// After prompt, save the session automatically
|
||||
await this._saveCurrentSession();
|
||||
break;
|
||||
case 'ready':
|
||||
await this._sendBrainStatus();
|
||||
await this._sendBrainProfiles();
|
||||
await this._sendSessionList();
|
||||
await this._sendModels();
|
||||
await this._restoreActiveSessionIntoView();
|
||||
break;
|
||||
case 'getModels':
|
||||
await this._sendModels();
|
||||
break;
|
||||
case 'newChat':
|
||||
this._currentSessionId = null;
|
||||
this._agent.clearHistory();
|
||||
this._currentSessionBrainId = getActiveBrainProfile().id;
|
||||
this._agent.resetConversation();
|
||||
await this._context.globalState.update(SidebarChatProvider.activeSessionStateKey, null);
|
||||
await this._context.globalState.update(SidebarChatProvider.lastVisibleChatStateKey, null);
|
||||
await this._context.globalState.update(SidebarChatProvider.blankChatStateKey, true);
|
||||
this.clearChat();
|
||||
await this._sendBrainStatus();
|
||||
break;
|
||||
case 'stopGeneration':
|
||||
this._agent.stop();
|
||||
@@ -82,23 +111,81 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
case 'openSettings':
|
||||
vscode.commands.executeCommand('workbench.action.openSettings', 'g1nation');
|
||||
break;
|
||||
case 'manageBrains':
|
||||
await this._manageBrains();
|
||||
break;
|
||||
case 'syncBrain':
|
||||
await this.syncBrain();
|
||||
await this._sendBrainStatus();
|
||||
break;
|
||||
case 'setBrainProfile':
|
||||
await this._setActiveBrainProfile(data.id);
|
||||
break;
|
||||
case 'refreshModels':
|
||||
await this._sendModels();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _currentSessionId: string | null = null;
|
||||
|
||||
private async _restoreActiveSessionIntoView() {
|
||||
if (!this._view) return;
|
||||
|
||||
const blankChatActive = this._context.globalState.get<boolean>(SidebarChatProvider.blankChatStateKey, false);
|
||||
if (blankChatActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeSessionId = this._currentSessionId || this._context.globalState.get<string | null>(SidebarChatProvider.activeSessionStateKey, null);
|
||||
if (activeSessionId) {
|
||||
const loaded = await this._loadSession(activeSessionId, true);
|
||||
if (loaded) return;
|
||||
|
||||
await this._context.globalState.update(SidebarChatProvider.activeSessionStateKey, null);
|
||||
}
|
||||
|
||||
const currentHistory = this._agent.getHistory();
|
||||
if (currentHistory.length > 0) {
|
||||
this._view.webview.postMessage({ type: 'restoreHistory', value: currentHistory });
|
||||
await this._persistLastVisibleChat(currentHistory);
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = this._context.globalState.get<LastVisibleChatSnapshot | null>(SidebarChatProvider.lastVisibleChatStateKey, null);
|
||||
if (snapshot?.history?.length) {
|
||||
this._currentSessionId = snapshot.sessionId || null;
|
||||
this._currentSessionBrainId = snapshot.brainProfileId || getActiveBrainProfile().id;
|
||||
await this._setActiveBrainProfile(this._currentSessionBrainId, true);
|
||||
this._agent.setHistory(snapshot.history);
|
||||
this._view.webview.postMessage({ type: 'restoreHistory', value: snapshot.history });
|
||||
}
|
||||
}
|
||||
|
||||
private async _persistLastVisibleChat(history: ChatMessage[] = this._agent.getHistory()) {
|
||||
if (history.length === 0) {
|
||||
await this._context.globalState.update(SidebarChatProvider.lastVisibleChatStateKey, null);
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot: LastVisibleChatSnapshot = {
|
||||
history,
|
||||
brainProfileId: this._currentSessionBrainId || getActiveBrainProfile().id,
|
||||
sessionId: this._currentSessionId,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
await this._context.globalState.update(SidebarChatProvider.lastVisibleChatStateKey, snapshot);
|
||||
}
|
||||
|
||||
private async _saveCurrentSession() {
|
||||
const history = this._agent.getHistory();
|
||||
if (history.length === 0) return;
|
||||
|
||||
let sessions = this._context.globalState.get<any[]>('chat_sessions', []) || [];
|
||||
let sessions = this._getSessions();
|
||||
const firstMsg = history.find(m => m.role === 'user')?.content;
|
||||
const title = typeof firstMsg === 'string' ? firstMsg.substring(0, 50).replace(/\n/g, ' ') + (firstMsg.length > 50 ? '...' : '') : 'New Chat';
|
||||
const brainProfileId = this._currentSessionBrainId || getActiveBrainProfile().id;
|
||||
|
||||
if (!this._currentSessionId) {
|
||||
this._currentSessionId = Date.now().toString();
|
||||
@@ -106,66 +193,313 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
id: this._currentSessionId,
|
||||
title,
|
||||
timestamp: Date.now(),
|
||||
history
|
||||
history,
|
||||
brainProfileId
|
||||
});
|
||||
} else {
|
||||
const idx = sessions.findIndex(s => s.id === this._currentSessionId);
|
||||
if (idx >= 0) {
|
||||
sessions[idx].history = history;
|
||||
sessions[idx].timestamp = Date.now();
|
||||
sessions[idx].brainProfileId = brainProfileId;
|
||||
if (!sessions[idx].title || sessions[idx].title === 'New Chat') {
|
||||
sessions[idx].title = title;
|
||||
}
|
||||
} else {
|
||||
sessions.unshift({
|
||||
id: this._currentSessionId,
|
||||
title,
|
||||
timestamp: Date.now(),
|
||||
history,
|
||||
brainProfileId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Keep only last 50 sessions
|
||||
if (sessions.length > 50) sessions = sessions.slice(0, 50);
|
||||
|
||||
await this._context.globalState.update('chat_sessions', sessions);
|
||||
await this._putSessions(sessions);
|
||||
await this._context.globalState.update(SidebarChatProvider.activeSessionStateKey, this._currentSessionId);
|
||||
await this._persistLastVisibleChat(history);
|
||||
await this._sendSessionList();
|
||||
}
|
||||
|
||||
private async _sendSessionList() {
|
||||
if (!this._view) return;
|
||||
const sessions = this._context.globalState.get<any[]>('chat_sessions', []) || [];
|
||||
const list = sessions.map(s => ({ id: s.id, title: s.title, timestamp: s.timestamp }));
|
||||
const sessions = this._getSessions();
|
||||
const list = sessions.map(s => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
timestamp: s.timestamp,
|
||||
brainProfileId: s.brainProfileId || '',
|
||||
messageCount: s.history.length,
|
||||
history: s.history
|
||||
}));
|
||||
this._view.webview.postMessage({ type: 'sessionList', value: list });
|
||||
}
|
||||
|
||||
private async _loadSession(id: string) {
|
||||
const sessions = this._context.globalState.get<any[]>('chat_sessions', []) || [];
|
||||
private async _loadSession(id: string, skipSessionListRefresh: boolean = false): Promise<boolean> {
|
||||
if (!id) {
|
||||
logError('Session load requested without an id.');
|
||||
this._view?.webview.postMessage({ type: 'error', value: 'Chat session id is missing.' });
|
||||
return false;
|
||||
}
|
||||
|
||||
const sessions = this._getSessions();
|
||||
const session = sessions.find(s => s.id === id);
|
||||
if (session) {
|
||||
const history = Array.isArray(session.history) ? session.history : [];
|
||||
if (history.length === 0) {
|
||||
logError('Session load failed because history is empty or invalid.', { id });
|
||||
this._view?.webview.postMessage({ type: 'error', value: 'This chat session has no saved messages.' });
|
||||
return false;
|
||||
}
|
||||
|
||||
this._agent.stop();
|
||||
this._currentSessionId = id;
|
||||
this._agent.setHistory(session.history);
|
||||
this._view?.webview.postMessage({ type: 'clearChat' });
|
||||
this._view?.webview.postMessage({ type: 'restoreHistory', value: session.history });
|
||||
const sessionBrainId = session.brainProfileId || getActiveBrainProfile().id;
|
||||
await this._setActiveBrainProfile(sessionBrainId, true);
|
||||
this._agent.setHistory(history);
|
||||
await this._context.globalState.update(SidebarChatProvider.activeSessionStateKey, id);
|
||||
await this._context.globalState.update(SidebarChatProvider.blankChatStateKey, false);
|
||||
await this._persistLastVisibleChat(history);
|
||||
this._view?.webview.postMessage({
|
||||
type: 'sessionLoaded',
|
||||
value: {
|
||||
id,
|
||||
title: session.title || 'Chat Session',
|
||||
history
|
||||
}
|
||||
});
|
||||
if (!skipSessionListRefresh) {
|
||||
await this._sendSessionList();
|
||||
}
|
||||
logInfo('Chat session loaded.', { id, messages: history.length });
|
||||
return true;
|
||||
}
|
||||
|
||||
logError('Session load failed because id was not found.', { id, sessionCount: sessions.length });
|
||||
this._view?.webview.postMessage({ type: 'error', value: 'Chat session was not found.' });
|
||||
return false;
|
||||
}
|
||||
|
||||
private async _deleteSession(id: string) {
|
||||
let sessions = this._context.globalState.get<any[]>('chat_sessions', []) || [];
|
||||
let sessions = this._getSessions();
|
||||
sessions = sessions.filter(s => s.id !== id);
|
||||
await this._context.globalState.update('chat_sessions', sessions);
|
||||
await this._putSessions(sessions);
|
||||
if (this._currentSessionId === id) {
|
||||
this._currentSessionId = null;
|
||||
this._agent.clearHistory();
|
||||
this._agent.resetConversation();
|
||||
await this._context.globalState.update(SidebarChatProvider.activeSessionStateKey, null);
|
||||
await this._context.globalState.update(SidebarChatProvider.lastVisibleChatStateKey, null);
|
||||
await this._context.globalState.update(SidebarChatProvider.blankChatStateKey, true);
|
||||
this.clearChat();
|
||||
}
|
||||
await this._sendSessionList();
|
||||
}
|
||||
|
||||
private _getSessions(): ChatSession[] {
|
||||
const rawSessions = this._context.globalState.get<any[]>('chat_sessions', []) || [];
|
||||
return rawSessions
|
||||
.map((session, index): ChatSession | null => {
|
||||
const history = Array.isArray(session?.history)
|
||||
? session.history.filter((message: any) =>
|
||||
message
|
||||
&& (message.role === 'user' || message.role === 'assistant' || message.role === 'system')
|
||||
&& message.content !== undefined
|
||||
)
|
||||
: [];
|
||||
|
||||
if (!session?.id || history.length === 0) return null;
|
||||
|
||||
const firstMsg = history.find((message: ChatMessage) => message.role === 'user')?.content;
|
||||
const fallbackTitle = typeof firstMsg === 'string'
|
||||
? firstMsg.substring(0, 50).replace(/\n/g, ' ') + (firstMsg.length > 50 ? '...' : '')
|
||||
: `Chat ${index + 1}`;
|
||||
|
||||
return {
|
||||
id: String(session.id),
|
||||
title: String(session.title || fallbackTitle),
|
||||
timestamp: typeof session.timestamp === 'number' ? session.timestamp : Date.now(),
|
||||
history,
|
||||
brainProfileId: String(session.brainProfileId || getActiveBrainProfile().id)
|
||||
};
|
||||
})
|
||||
.filter((session): session is ChatSession => !!session)
|
||||
.sort((a, b) => b.timestamp - a.timestamp)
|
||||
.slice(0, 50);
|
||||
}
|
||||
|
||||
private async _putSessions(sessions: ChatSession[]) {
|
||||
await this._context.globalState.update('chat_sessions', sessions.slice(0, 50));
|
||||
}
|
||||
|
||||
private async _sendBrainStatus() {
|
||||
if (!this._view) return;
|
||||
const brainDir = _getBrainDir();
|
||||
const activeBrain = getActiveBrainProfile();
|
||||
const brainDir = activeBrain.localBrainPath;
|
||||
const files = findBrainFiles(brainDir);
|
||||
this._view.webview.postMessage({
|
||||
type: 'brainStatus',
|
||||
value: {
|
||||
count: files.length,
|
||||
path: brainDir
|
||||
path: brainDir,
|
||||
name: activeBrain.name,
|
||||
description: activeBrain.description || '',
|
||||
repo: activeBrain.secondBrainRepo || ''
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async _sendBrainProfiles() {
|
||||
if (!this._view) return;
|
||||
const activeBrain = getActiveBrainProfile();
|
||||
this._currentSessionBrainId = this._currentSessionBrainId || activeBrain.id;
|
||||
const profiles = getBrainProfiles().map((profile) => ({
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
path: profile.localBrainPath,
|
||||
description: profile.description || '',
|
||||
repo: profile.secondBrainRepo || ''
|
||||
}));
|
||||
this._view.webview.postMessage({
|
||||
type: 'brainProfiles',
|
||||
value: {
|
||||
activeBrainId: activeBrain.id,
|
||||
profiles
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async _setActiveBrainProfile(profileId: string, silent: boolean = false) {
|
||||
const profiles = getBrainProfiles();
|
||||
const nextProfile = profiles.find((profile) => profile.id === profileId) || profiles[0];
|
||||
if (!nextProfile) return;
|
||||
|
||||
await vscode.workspace.getConfiguration('g1nation').update('activeBrainId', nextProfile.id, vscode.ConfigurationTarget.Global);
|
||||
this._currentSessionBrainId = nextProfile.id;
|
||||
await this._sendBrainProfiles();
|
||||
await this._sendBrainStatus();
|
||||
|
||||
if (!silent) {
|
||||
this.injectSystemMessage(`**[Brain Switched]** ${nextProfile.name}\n\`${nextProfile.localBrainPath}\``);
|
||||
}
|
||||
}
|
||||
|
||||
private async _manageBrains() {
|
||||
const activeBrain = getActiveBrainProfile();
|
||||
const choice = await vscode.window.showQuickPick([
|
||||
{
|
||||
label: 'Add Brain Folder',
|
||||
description: 'Create a new brain profile from a local folder'
|
||||
},
|
||||
{
|
||||
label: 'Open Active Brain Folder',
|
||||
description: activeBrain.localBrainPath
|
||||
},
|
||||
{
|
||||
label: 'Open Brain Settings',
|
||||
description: 'Edit names, paths, repos, and descriptions'
|
||||
}
|
||||
], {
|
||||
placeHolder: `Active Brain: ${activeBrain.name} (${activeBrain.localBrainPath})`
|
||||
});
|
||||
|
||||
if (!choice) return;
|
||||
|
||||
if (choice.label === 'Add Brain Folder') {
|
||||
await this._addBrainProfile();
|
||||
return;
|
||||
}
|
||||
|
||||
if (choice.label === 'Open Active Brain Folder') {
|
||||
if (!fs.existsSync(activeBrain.localBrainPath)) {
|
||||
fs.mkdirSync(activeBrain.localBrainPath, { recursive: true });
|
||||
}
|
||||
await vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(activeBrain.localBrainPath));
|
||||
return;
|
||||
}
|
||||
|
||||
vscode.commands.executeCommand('workbench.action.openSettings', 'g1nation.brainProfiles');
|
||||
}
|
||||
|
||||
private async _addBrainProfile() {
|
||||
const selected = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: false,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false,
|
||||
openLabel: 'Use as Brain'
|
||||
});
|
||||
|
||||
const folder = selected?.[0]?.fsPath;
|
||||
if (!folder) return;
|
||||
|
||||
const defaultName = path.basename(folder) || 'New Brain';
|
||||
const name = await vscode.window.showInputBox({
|
||||
prompt: 'Name this brain profile',
|
||||
value: defaultName,
|
||||
validateInput: (value) => value.trim() ? null : 'Brain name is required.'
|
||||
});
|
||||
if (!name) return;
|
||||
|
||||
const description = await vscode.window.showInputBox({
|
||||
prompt: 'Optional description shown in the G1nation sidebar',
|
||||
value: ''
|
||||
});
|
||||
|
||||
const repo = await vscode.window.showInputBox({
|
||||
prompt: 'Optional Second Brain Git repository URL',
|
||||
value: ''
|
||||
});
|
||||
|
||||
// Read raw settings directly to avoid virtual default-brain (injected in memory by getConfig())
|
||||
// being saved into the settings file and corrupting the profile list on next load.
|
||||
const cfg = vscode.workspace.getConfiguration('g1nation');
|
||||
const existingRaw: any[] = cfg.get<any[]>('brainProfiles', []) || [];
|
||||
|
||||
const idBase = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'brain';
|
||||
let id = idBase;
|
||||
let suffix = 2;
|
||||
while (existingRaw.some((p: any) => p.id === id)) {
|
||||
id = `${idBase}-${suffix++}`;
|
||||
}
|
||||
|
||||
const newProfile = {
|
||||
id,
|
||||
name: name.trim(),
|
||||
localBrainPath: folder,
|
||||
secondBrainRepo: (repo || '').trim(),
|
||||
description: (description || '').trim()
|
||||
};
|
||||
const nextProfiles = [...existingRaw, newProfile];
|
||||
|
||||
await cfg.update('brainProfiles', nextProfiles, vscode.ConfigurationTarget.Global);
|
||||
await cfg.update('activeBrainId', id, vscode.ConfigurationTarget.Global);
|
||||
this._currentSessionBrainId = id;
|
||||
|
||||
// Directly post the freshly-built profile list to the webview.
|
||||
// cfg.update() is async and VSCode's config cache may not reflect the new value
|
||||
// immediately, so we avoid re-reading via getBrainProfiles() here.
|
||||
if (this._view) {
|
||||
this._view.webview.postMessage({
|
||||
type: 'brainProfiles',
|
||||
value: {
|
||||
activeBrainId: id,
|
||||
profiles: nextProfiles.map((p: any) => ({
|
||||
id: p.id || '',
|
||||
name: p.name || '',
|
||||
path: p.localBrainPath || '',
|
||||
description: p.description || '',
|
||||
repo: p.secondBrainRepo || ''
|
||||
}))
|
||||
}
|
||||
});
|
||||
}
|
||||
await this._sendBrainStatus();
|
||||
this.injectSystemMessage(`**[Brain Added]** ${name.trim()}\n\`${folder}\``);
|
||||
}
|
||||
|
||||
// --- BridgeInterface Methods ---
|
||||
|
||||
public injectSystemMessage(msg: string): void {
|
||||
@@ -200,7 +534,8 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
}
|
||||
|
||||
public async syncBrain() {
|
||||
const brainDir = _getBrainDir();
|
||||
const activeBrain = getActiveBrainProfile();
|
||||
const brainDir = activeBrain.localBrainPath;
|
||||
if (!fs.existsSync(brainDir)) {
|
||||
vscode.window.showErrorMessage("Second Brain directory not found.");
|
||||
return;
|
||||
@@ -226,6 +561,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
if (!this._view) return;
|
||||
|
||||
const { value, model, internet, files } = data;
|
||||
this._currentSessionBrainId = getActiveBrainProfile().id;
|
||||
|
||||
try {
|
||||
await this._agent.handlePrompt(value, model, {
|
||||
@@ -275,13 +611,19 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
models = [defaultModel];
|
||||
models = defaultModel ? [defaultModel] : [];
|
||||
this._view.webview.postMessage({ type: 'engineStatus', value: { online: false, url } });
|
||||
} else {
|
||||
this._view.webview.postMessage({ type: 'engineStatus', value: { online: true, url } });
|
||||
}
|
||||
|
||||
if (models.length > 0 && !models.includes(defaultModel)) {
|
||||
const baseModel = defaultModel?.replace(/:\d+$/, '');
|
||||
if (baseModel && baseModel !== defaultModel && models.includes(baseModel)) {
|
||||
defaultModel = baseModel;
|
||||
await vscode.workspace.getConfiguration('g1nation').update('defaultModel', defaultModel, vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
|
||||
if (models.length > 0 && (!defaultModel || !models.includes(defaultModel))) {
|
||||
defaultModel = models[0];
|
||||
await vscode.workspace.getConfiguration('g1nation').update('defaultModel', defaultModel, vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
@@ -292,10 +634,11 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
models.unshift(defaultModel);
|
||||
}
|
||||
|
||||
this._view.webview.postMessage({ type: 'modelsList', value: models });
|
||||
this._view.webview.postMessage({ type: 'modelsList', value: { models, selected: defaultModel } });
|
||||
} catch (err) {
|
||||
logError('Model list update failed.', err);
|
||||
this._view.webview.postMessage({ type: 'modelsList', value: [getConfig().defaultModel] });
|
||||
const fallbackModel = getConfig().defaultModel;
|
||||
this._view.webview.postMessage({ type: 'modelsList', value: { models: fallbackModel ? [fallbackModel] : [], selected: fallbackModel } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,11 +674,17 @@ body::before{content:'';position:fixed;top:-50%;left:-50%;width:200%;height:200%
|
||||
.thinking-bar.active{background:rgba(124,106,255,.1)}
|
||||
.thinking-bar.active::after{content:'';position:absolute;top:0;left:-40%;width:40%;height:100%;background:linear-gradient(90deg,transparent,var(--accent),var(--accent2),var(--accent3),transparent);animation:thinkSlide 1.5s ease-in-out infinite}
|
||||
@keyframes thinkSlide{0%{left:-40%}100%{left:100%}}
|
||||
.brain-strip{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:7px 14px;border-bottom:1px solid var(--border);background:rgba(0,10,2,.64);z-index:8;position:relative}
|
||||
.brain-strip-main{font-size:11px;color:var(--accent);font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.brain-strip-sub{font-size:10px;color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:45%}
|
||||
.header-left{display:flex;align-items:center;gap:8px}
|
||||
.logo{width:26px;height:26px;border-radius:6px;background:#050505;border:1px solid rgba(0,255,65,.3);display:flex;align-items:center;justify-content:center;font-size:16px;color:var(--accent);box-shadow:0 0 15px rgba(0,255,65,.15);animation:logoPulse 3s ease-in-out infinite;position:relative;text-shadow:0 0 8px var(--accent)}
|
||||
@keyframes logoPulse{0%,100%{box-shadow:0 0 10px rgba(0,255,65,.1)}50%{box-shadow:0 0 25px rgba(0,255,65,.3)}}
|
||||
.brand{font-weight:800;font-size:14px;color:var(--text-bright);letter-spacing:-.5px;background:linear-gradient(135deg,#fff 40%,var(--accent) 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.header-right{display:flex;align-items:center;gap:5px}
|
||||
.header-right{display:flex;align-items:center;gap:5px;flex-wrap:wrap;justify-content:flex-end}
|
||||
.select-wrap{display:flex;align-items:center;gap:5px}
|
||||
.select-wrap select{max-width:140px}
|
||||
.brain-meta{font-size:10px;color:var(--text-dim);max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.history-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);backdrop-filter:blur(15px);z-index:100;display:none;flex-direction:column;padding:20px;animation:fadeIn 0.3s ease-out}
|
||||
.history-overlay.visible{display:flex}
|
||||
.history-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}
|
||||
@@ -382,6 +731,7 @@ select:hover,select:focus{border-color:var(--accent);box-shadow:0 0 12px var(--a
|
||||
.loading-dots span:nth-child(3){animation-delay:.4s;background:var(--accent3)}
|
||||
@keyframes dotBounce{0%,80%,100%{transform:scale(.6);opacity:.4}40%{transform:scale(1.2);opacity:1}}
|
||||
.loading-text{font-size:11px;color:var(--text-dim);animation:pulse 2s ease-in-out infinite;letter-spacing:.3px}
|
||||
.auto-status{margin:12px 0 4px 29px;padding:10px 12px;border:1px solid rgba(255,255,255,.16);border-radius:10px;background:rgba(0,255,65,.08);color:#fff;font-size:13px;font-weight:700;line-height:1.5;box-shadow:0 0 18px rgba(0,255,65,.12)}
|
||||
.input-wrap{padding:8px 14px 14px;flex-shrink:0;position:relative;z-index:1}
|
||||
.input-box{background:var(--input-bg);border:1px solid var(--border2);border-radius:14px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;transition:all .3s;position:relative;backdrop-filter:blur(12px)}
|
||||
.input-box:focus-within{border-color:var(--accent);box-shadow:0 0 24px rgba(0,255,65,.15)}
|
||||
@@ -414,19 +764,19 @@ body.init .input-wrap{max-width:680px;width:100%;margin:0 auto}
|
||||
.regen-btn{display:inline-flex;align-items:center;gap:4px;background:transparent;border:none;color:var(--text-dim);padding:4px 6px;border-radius:4px;font-size:11px;cursor:pointer;transition:color 0.2s;margin-top:6px;margin-left:29px;opacity:0.7}
|
||||
.regen-btn:hover{color:var(--text);opacity:1}
|
||||
</style></head><body class="init">
|
||||
<div class="header"><div class="header-left"><div class="logo">✦</div><span class="brand">G1nation</span></div><div class="header-right"><div id="engineStatusDot" style="width:8px;height:8px;border-radius:50%;background:var(--text-dim);margin-right:-2px" title="Checking AI Engine..."></div><select id="modelSel"></select><button class="btn-icon" id="internetBtn" title="Internet Access: OFF">🌐</button><button class="btn-icon" id="brainBtn" title="Sync Second Brain">🧠<span id="brainCountBadge" style="position:absolute;top:-5px;right:-5px;background:var(--accent);color:#000;font-size:8px;padding:1px 3px;border-radius:4px;display:none;font-weight:bold">0</span></button><button class="btn-icon" id="historyBtn" title="Chat History">📜</button><button class="btn-icon" id="settingsBtn" title="Settings">⚙️</button><button class="btn-icon" id="newChatBtn" title="New Chat">+</button></div></div>
|
||||
<div class="header"><div class="header-left"><div class="logo">✦</div><span class="brand">G1nation</span></div><div class="header-right"><div id="engineStatusDot" style="width:8px;height:8px;border-radius:50%;background:var(--text-dim);margin-right:-2px" title="Checking AI Engine..."></div><div class="select-wrap"><select id="modelSel" title="Active AI model"></select><button class="btn-icon" id="refreshModelsBtn" title="Refresh Models">↻</button></div><div class="select-wrap"><select id="brainSel" title="Active Second Brain"></select><button class="btn-icon" id="manageBrainsBtn" title="Add or manage brains">✎</button></div><button class="btn-icon" id="internetBtn" title="Internet Access: OFF">🌐</button><button class="btn-icon" id="brainBtn" title="Sync Second Brain">🧠<span id="brainCountBadge" style="position:absolute;top:-5px;right:-5px;background:var(--accent);color:#000;font-size:8px;padding:1px 3px;border-radius:4px;display:none;font-weight:bold">0</span></button><button class="btn-icon" id="historyBtn" title="Chat History">📜</button><button class="btn-icon" id="settingsBtn" title="Settings">⚙️</button><button class="btn-icon" id="newChatBtn" title="New Chat">+</button></div></div>
|
||||
<div id="historyOverlay" class="history-overlay">
|
||||
<div class="history-header"><span class="history-title">Chat Sessions</span><button class="btn-icon" id="closeHistoryBtn">✕</button></div>
|
||||
<div class="history-list" id="historyList"></div>
|
||||
</div>
|
||||
<div class="thinking-bar" id="thinkingBar"></div>
|
||||
<div class="brain-strip"><div id="brainStatusInfo" class="brain-strip-main">Brain: checking...</div><div id="brainStatusMeta" class="brain-strip-sub"></div></div>
|
||||
<div class="main-view" id="mainView">
|
||||
<div class="chat" id="chat">
|
||||
<div class="welcome">
|
||||
<div class="welcome-logo">✦</div>
|
||||
<div class="welcome-title">G1nation</div>
|
||||
<div class="welcome-sub">Security · Optimized · Knowledge Mesh<br>Understands your project, writes code, and executes tasks.</div>
|
||||
<div id="brainStatusInfo" style="margin-top:10px;font-size:11px;color:var(--accent);opacity:0.8;display:none"></div>
|
||||
<div class="welcome-sub">Security · Optimized · Knowledge Mesh<br>Choose a brain above, then ask what it knows.</div>
|
||||
</div></div>
|
||||
<div class="input-wrap"><div class="input-box">
|
||||
<div class="attach-preview" id="attachPreview"></div>
|
||||
@@ -439,7 +789,7 @@ body.init .input-wrap{max-width:680px;width:100%;margin:0 auto}
|
||||
try {
|
||||
const vscode=acquireVsCodeApi(),chat=document.getElementById('chat'),input=document.getElementById('input'),
|
||||
sendBtn=document.getElementById('sendBtn'),stopBtn=document.getElementById('stopBtn'),
|
||||
modelSel=document.getElementById('modelSel'),newChatBtn=document.getElementById('newChatBtn'),settingsBtn=document.getElementById('settingsBtn'),brainBtn=document.getElementById('brainBtn'),
|
||||
modelSel=document.getElementById('modelSel'),brainSel=document.getElementById('brainSel'),refreshModelsBtn=document.getElementById('refreshModelsBtn'),manageBrainsBtn=document.getElementById('manageBrainsBtn'),newChatBtn=document.getElementById('newChatBtn'),settingsBtn=document.getElementById('settingsBtn'),brainBtn=document.getElementById('brainBtn'),
|
||||
historyBtn=document.getElementById('historyBtn'),historyOverlay=document.getElementById('historyOverlay'),closeHistoryBtn=document.getElementById('closeHistoryBtn'),historyList=document.getElementById('historyList'),
|
||||
internetBtn=document.getElementById('internetBtn'),attachBtn=document.getElementById('attachBtn'),fileInput=document.getElementById('fileInput'),attachPreview=document.getElementById('attachPreview'),
|
||||
thinkingBar=document.getElementById('thinkingBar');
|
||||
@@ -447,6 +797,9 @@ let loader=null,sending=false,pendingFiles=[],internetEnabled=false;
|
||||
|
||||
historyBtn.addEventListener('click', ()=>historyOverlay.classList.add('visible'));
|
||||
closeHistoryBtn.addEventListener('click', ()=>historyOverlay.classList.remove('visible'));
|
||||
refreshModelsBtn.addEventListener('click', ()=>vscode.postMessage({type:'refreshModels'}));
|
||||
manageBrainsBtn.addEventListener('click', ()=>vscode.postMessage({type:'manageBrains'}));
|
||||
brainSel.addEventListener('change', ()=>vscode.postMessage({type:'setBrainProfile',id:brainSel.value}));
|
||||
|
||||
internetBtn.addEventListener('click', ()=>{
|
||||
internetEnabled=!internetEnabled;
|
||||
@@ -467,6 +820,27 @@ function fmt(t){
|
||||
return formatted;
|
||||
}
|
||||
|
||||
function normalizeMsgContent(content){
|
||||
if(Array.isArray(content)) return content.map(part=>typeof part==='string'?part:(part?.text||part?.name||JSON.stringify(part))).join('\\n');
|
||||
return String(content||'');
|
||||
}
|
||||
|
||||
function resetChatView(){
|
||||
chat.innerHTML='';
|
||||
streamBody=null;hideLoader();setSending(false);
|
||||
document.body.classList.remove('init');
|
||||
}
|
||||
|
||||
function renderHistory(history){
|
||||
resetChatView();
|
||||
(history||[]).filter(m => !m.internal).forEach(m => addMsg(normalizeMsgContent(m.content), m.role === 'assistant' ? 'ai' : 'user'));
|
||||
if((history||[]).length===0){
|
||||
chat.innerHTML='<div class="welcome"><div class="welcome-logo">✦</div><div class="welcome-title">G1nation</div><div class="welcome-sub">No messages in this session.</div></div>';
|
||||
document.body.classList.add('init');
|
||||
}
|
||||
chat.scrollTop=chat.scrollHeight;
|
||||
}
|
||||
|
||||
function addMsg(text,role){
|
||||
const isUser=role==='user',isErr=role==='error';
|
||||
const el=document.createElement('div');el.className='msg'+(isUser?' msg-user':'')+(isErr?' msg-error':'');
|
||||
@@ -488,7 +862,7 @@ function send(){
|
||||
const w=document.querySelector('.welcome');if(w)w.remove();
|
||||
addMsg(text,'user');
|
||||
input.value='';input.style.height='auto';setSending(true);showLoader();
|
||||
vscode.postMessage({type:'prompt',value:text,model:modelSel.value,internet:internetEnabled,files:pendingFiles});
|
||||
vscode.postMessage({type:'prompt',value:text,model:modelSel.value,internet:internetEnabled,files:pendingFiles.length?pendingFiles:undefined});
|
||||
pendingFiles=[];renderPreview();
|
||||
}
|
||||
|
||||
@@ -525,22 +899,32 @@ settingsBtn.addEventListener('click',()=>vscode.postMessage({type:'openSettings'
|
||||
brainBtn.addEventListener('click',()=>vscode.postMessage({type:'syncBrain'}));
|
||||
stopBtn.addEventListener('click',()=>{vscode.postMessage({type:'stopGeneration'});hideLoader();setSending(false);});
|
||||
|
||||
let streamBody=null;
|
||||
let streamBody=null,sessionCache={};
|
||||
window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
|
||||
case 'restoreHistory':
|
||||
chat.innerHTML='';
|
||||
document.body.classList.remove('init');
|
||||
msg.value.filter(m => !m.internal).forEach(m => addMsg(m.content, m.role === 'assistant' ? 'ai' : 'user'));
|
||||
renderHistory(msg.value||[]);
|
||||
break;
|
||||
case 'sessionLoaded':
|
||||
renderHistory(msg.value.history||[]);
|
||||
historyOverlay.classList.remove('visible');
|
||||
break;
|
||||
case 'sessionList':
|
||||
historyList.innerHTML='';
|
||||
sessionCache={};
|
||||
if(!msg.value || msg.value.length===0){
|
||||
historyList.innerHTML='<div class="history-item"><div class="history-item-info"><div class="history-item-title">No saved chats yet</div><div class="history-item-date">Start a conversation and it will appear here.</div></div></div>';
|
||||
break;
|
||||
}
|
||||
msg.value.forEach(s=>{
|
||||
sessionCache[s.id]=s;
|
||||
const el=document.createElement('div');el.className='history-item';
|
||||
el.innerHTML='<div class="history-item-info"><div class="history-item-title">'+esc(s.title)+'</div><div class="history-item-date">'+new Date(s.timestamp).toLocaleString()+'</div></div><button class="history-del-btn" title="Delete">🗑</button>';
|
||||
el.dataset.sessionId=s.id;
|
||||
el.innerHTML='<div class="history-item-info"><div class="history-item-title">'+esc(s.title)+'</div><div class="history-item-date">'+new Date(s.timestamp).toLocaleString()+' · '+(s.messageCount||0)+' messages</div></div><button class="history-del-btn" title="Delete">🗑</button>';
|
||||
el.addEventListener('click',(e)=>{
|
||||
if(e.target.classList.contains('history-del-btn')) return;
|
||||
if(e.target.closest('.history-del-btn')) return;
|
||||
const cached=sessionCache[s.id];
|
||||
if(cached && cached.history){renderHistory(cached.history);historyOverlay.classList.remove('visible');}
|
||||
vscode.postMessage({type:'loadSession',id:s.id});
|
||||
historyOverlay.classList.remove('visible');
|
||||
});
|
||||
el.querySelector('.history-del-btn').addEventListener('click',()=>{
|
||||
vscode.postMessage({type:'deleteSession',id:s.id});
|
||||
@@ -563,7 +947,16 @@ window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
|
||||
break;
|
||||
case 'modelsList':
|
||||
modelSel.innerHTML='';
|
||||
msg.value.forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;modelSel.appendChild(o)});
|
||||
(msg.value.models||[]).forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;modelSel.appendChild(o)});
|
||||
if(msg.value.selected) modelSel.value=msg.value.selected;
|
||||
break;
|
||||
case 'brainProfiles':
|
||||
brainSel.innerHTML='';
|
||||
(msg.value.profiles||[]).forEach(profile=>{
|
||||
const o=document.createElement('option');o.value=profile.id;o.textContent=profile.name;brainSel.appendChild(o);
|
||||
o.title=profile.path;
|
||||
});
|
||||
if(msg.value.activeBrainId) brainSel.value=msg.value.activeBrainId;
|
||||
break;
|
||||
case 'engineStatus':
|
||||
const dot = document.getElementById('engineStatusDot');
|
||||
@@ -575,18 +968,24 @@ window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
|
||||
case 'brainStatus':
|
||||
const badge = document.getElementById('brainCountBadge');
|
||||
const info = document.getElementById('brainStatusInfo');
|
||||
const meta = document.getElementById('brainStatusMeta');
|
||||
if(badge){
|
||||
badge.innerText = msg.value.count > 999 ? '999+' : msg.value.count;
|
||||
badge.style.display = msg.value.count > 0 ? 'block' : 'none';
|
||||
}
|
||||
if(info){
|
||||
info.innerText = \`🧠 Second Brain: \${msg.value.count} knowledge files connected\`;
|
||||
info.style.display = 'block';
|
||||
info.innerText = \`Brain: \${msg.value.name} · \${msg.value.count} files\`;
|
||||
info.title = msg.value.path;
|
||||
}
|
||||
const brainBtn = document.getElementById('brainBtn');
|
||||
if(brainBtn) brainBtn.title = \`Sync Brain (Connected: \${msg.value.count} files at \${msg.value.path})\`;
|
||||
if(meta){
|
||||
meta.innerText = msg.value.description ? \`\${msg.value.description} · \${msg.value.path}\` : msg.value.path;
|
||||
meta.title = msg.value.path;
|
||||
}
|
||||
const syncBrainBtn = document.getElementById('brainBtn');
|
||||
if(syncBrainBtn) syncBrainBtn.title = \`Sync Brain: \${msg.value.name} (\${msg.value.count} files at \${msg.value.path})\`;
|
||||
break;
|
||||
case 'clearChat':
|
||||
streamBody=null;hideLoader();setSending(false);
|
||||
chat.innerHTML='<div class="welcome"><div class="welcome-logo">✦</div><div class="welcome-title">G1nation</div><div class="welcome-sub">System Cleaned.</div></div>';
|
||||
document.body.classList.add('init');
|
||||
break;
|
||||
@@ -594,11 +993,9 @@ window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
|
||||
input.value=msg.value;send();
|
||||
break;
|
||||
case 'autoContinue':
|
||||
showLoader();
|
||||
showLoader();setSending(true);
|
||||
const hint = document.createElement('div');
|
||||
hint.className = 'input-hint';
|
||||
hint.style.textAlign = 'center';
|
||||
hint.style.margin = '10px 0';
|
||||
hint.className = 'auto-status';
|
||||
hint.innerText = msg.value;
|
||||
chat.appendChild(hint);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
|
||||
+119
-24
@@ -11,14 +11,91 @@ export const EXCLUDED_DIRS = new Set([
|
||||
|
||||
// Configuration constants moved to package.json and getConfig()
|
||||
|
||||
export interface BrainProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
localBrainPath: string;
|
||||
secondBrainRepo?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
function normalizePath(p: string): string {
|
||||
if (!p) return p;
|
||||
if (p.startsWith('~/')) {
|
||||
return path.join(os.homedir(), p.substring(2));
|
||||
}
|
||||
return p.trim();
|
||||
}
|
||||
|
||||
function toBrainProfile(raw: Partial<BrainProfile> | undefined, fallbackIndex: number): BrainProfile | null {
|
||||
if (!raw) return null;
|
||||
const localBrainPath = normalizePath(raw.localBrainPath || '');
|
||||
if (!localBrainPath) return null;
|
||||
return {
|
||||
id: (raw.id || `brain-${fallbackIndex + 1}`).trim(),
|
||||
name: (raw.name || path.basename(localBrainPath) || `Brain ${fallbackIndex + 1}`).trim(),
|
||||
localBrainPath,
|
||||
secondBrainRepo: (raw.secondBrainRepo || '').trim(),
|
||||
description: (raw.description || '').trim()
|
||||
};
|
||||
}
|
||||
|
||||
const STEVE_SKILL_PATH = '/Volumes/Data/project/Antigravity/Agent/.agent/skills/steve_jobs/SKILL.md';
|
||||
|
||||
function extractSteveRuntimeSection(skillContent: string): string {
|
||||
const sectionStart = skillContent.indexOf('## 🎯 Steve Single-Contact Runtime Protocol');
|
||||
if (sectionStart < 0) return '';
|
||||
const sectionEnd = skillContent.indexOf('### 4. The "One More Thing" Mandate', sectionStart);
|
||||
return skillContent.slice(sectionStart, sectionEnd > sectionStart ? sectionEnd : undefined).trim();
|
||||
}
|
||||
|
||||
function loadSteveRuntimePrompt(): string {
|
||||
try {
|
||||
if (!fs.existsSync(STEVE_SKILL_PATH)) return '';
|
||||
const skillContent = fs.readFileSync(STEVE_SKILL_PATH, 'utf-8');
|
||||
return extractSteveRuntimeSection(skillContent);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function getConfig() {
|
||||
const cfg = vscode.workspace.getConfiguration('g1nation');
|
||||
const legacyBrainPath = cfg.get<string>('localBrainPath', '');
|
||||
const legacyBrainRepo = cfg.get<string>('secondBrainRepo', '');
|
||||
const configuredProfiles = cfg.get<Partial<BrainProfile>[]>('brainProfiles', []);
|
||||
const profiles = configuredProfiles
|
||||
.map((profile, index) => toBrainProfile(profile, index))
|
||||
.filter((profile): profile is BrainProfile => !!profile);
|
||||
|
||||
// IMPORTANT: This virtual default-brain exists only in memory at runtime.
|
||||
// It must NEVER be written back to the settings file (g1nation.brainProfiles).
|
||||
// _addBrainProfile() reads cfg.get('brainProfiles') directly to avoid this contamination.
|
||||
if (profiles.length === 0) {
|
||||
const fallbackPath = normalizePath(legacyBrainPath) || path.join(os.homedir(), '.g1nation-brain');
|
||||
profiles.push({
|
||||
id: 'default-brain',
|
||||
name: 'Local Brain',
|
||||
localBrainPath: fallbackPath,
|
||||
secondBrainRepo: legacyBrainRepo.trim(),
|
||||
description: legacyBrainPath
|
||||
? 'Migrated from your existing localBrainPath setting'
|
||||
: 'Auto-created local knowledge folder. Add a real brain via the ✎ button.'
|
||||
});
|
||||
}
|
||||
|
||||
const activeBrainId = cfg.get<string>('activeBrainId', profiles[0].id) || profiles[0].id;
|
||||
const activeBrain = profiles.find((profile) => profile.id === activeBrainId) || profiles[0];
|
||||
|
||||
return {
|
||||
ollamaUrl: cfg.get<string>('ollamaUrl', 'http://127.0.0.1:11434'),
|
||||
defaultModel: cfg.get<string>('defaultModel', 'gemma4:e2b'),
|
||||
maxTreeFiles: 200,
|
||||
timeout: cfg.get<number>('requestTimeout', 300) * 1000,
|
||||
localBrainPath: cfg.get<string>('localBrainPath', ''),
|
||||
localBrainPath: activeBrain.localBrainPath,
|
||||
secondBrainRepo: activeBrain.secondBrainRepo || '',
|
||||
brainProfiles: profiles,
|
||||
activeBrainId: activeBrain.id,
|
||||
maxContextSize: cfg.get<number>('maxContextSize', 12000),
|
||||
maxAutoSteps: cfg.get<number>('maxAutoSteps', 50)
|
||||
};
|
||||
@@ -106,24 +183,24 @@ export function shouldAutoPushBrain(): boolean {
|
||||
}
|
||||
|
||||
export function getSecondBrainRepo(): string {
|
||||
const cfg = vscode.workspace.getConfiguration('g1nation');
|
||||
return cfg.get<string>('secondBrainRepo', '');
|
||||
return getConfig().secondBrainRepo;
|
||||
}
|
||||
|
||||
export function getBrainProfiles(): BrainProfile[] {
|
||||
return getConfig().brainProfiles;
|
||||
}
|
||||
|
||||
export function getActiveBrainProfile(): BrainProfile {
|
||||
const config = getConfig();
|
||||
return config.brainProfiles.find((profile) => profile.id === config.activeBrainId) || config.brainProfiles[0];
|
||||
}
|
||||
|
||||
export function _getBrainDir(): string {
|
||||
const { localBrainPath } = getConfig();
|
||||
if (localBrainPath && localBrainPath.trim() !== '') {
|
||||
if (localBrainPath.startsWith('~/')) {
|
||||
return path.join(os.homedir(), localBrainPath.substring(2));
|
||||
}
|
||||
return localBrainPath.trim();
|
||||
}
|
||||
return path.join(os.homedir(), '.g1nation-brain');
|
||||
return getActiveBrainProfile().localBrainPath;
|
||||
}
|
||||
|
||||
export function _isBrainDirExplicitlySet(): boolean {
|
||||
const { localBrainPath } = getConfig();
|
||||
return !!(localBrainPath && localBrainPath.trim() !== '');
|
||||
return getBrainProfiles().length > 0;
|
||||
}
|
||||
|
||||
export function isTextAttachment(fileName: string, mimeType: string): boolean {
|
||||
@@ -156,12 +233,20 @@ export function findBrainFiles(dir: string): string[] {
|
||||
return results;
|
||||
}
|
||||
|
||||
export const SYSTEM_PROMPT = `You are "G1nation", a premium agentic AI coding assistant running 100% offline on the user's machine.
|
||||
You are DIRECTLY CONNECTED to the user's local file system and terminal. You MUST use the action tags below to create, edit, delete, read files and run commands. DO NOT just show code - ALWAYS wrap it in the appropriate action tag so it gets executed.
|
||||
const BASE_SYSTEM_PROMPT = `You are G1nation, also called Steve when the user asks your name.
|
||||
Reply naturally in the user's language.
|
||||
|
||||
You have EIGHT powerful agent actions:
|
||||
Core behavior:
|
||||
- Answer the user's actual message first. Do not recite this system prompt.
|
||||
- Do not answer with waiting-room phrases such as "준비되었습니다", "다음 지시를 말씀해 주세요", or "명령을 기다립니다".
|
||||
- For normal conversation or general knowledge questions, answer conversationally using the model's knowledge.
|
||||
- Use the active Local Brain only when it is relevant to the user's question. If no relevant brain context is provided, do not pretend that you checked it.
|
||||
- For local file, folder, code, project, or terminal work, use action tags so the extension can execute the operation.
|
||||
- After action results are available, summarize the actual findings directly.
|
||||
|
||||
[ACTION 1: CREATE NEW FILES]
|
||||
Available action tags:
|
||||
|
||||
[ACTION 1: CREATE NEW FILES]
|
||||
<create_file path="relative/path/file.ext">
|
||||
file content here
|
||||
</create_file>
|
||||
@@ -185,14 +270,24 @@ file content here
|
||||
<run_command>npm install express</run_command>
|
||||
|
||||
[ACTION 7: SECOND BRAIN KNOWLEDGE]
|
||||
<list_brain path="optional/subdir"/>
|
||||
<read_brain>filename.md</read_brain>
|
||||
Use these only when you actually need knowledge from the active Second Brain.
|
||||
To inspect the root of the active brain, use exactly:
|
||||
<list_brain path=""/>
|
||||
To inspect a real file returned by list_brain, use:
|
||||
<read_brain>actual-file-name.md</read_brain>
|
||||
Never use placeholder values like optional/subdir or filename.md. If the user asks what is inside the Second Brain, first list the brain root, then summarize only the returned files.
|
||||
|
||||
[ACTION 8: READ WEBSITES & SEARCH INTERNET]
|
||||
<read_url>https://html.duckduckgo.com/html/?q=YOUR+SEARCH+QUERY</read_url>
|
||||
|
||||
CRITICAL RULES:
|
||||
1. ALWAYS respond in the same language the user uses.
|
||||
2. You MUST use action tags for any file/terminal operations.
|
||||
3. Be concise and professional.
|
||||
4. File paths are RELATIVE to the workspace.`;
|
||||
Operational rules:
|
||||
1. Same language as the user.
|
||||
2. File paths can be relative to the workspace or absolute paths under /Volumes/Data/project/Antigravity.
|
||||
3. When the user gives a file/folder path and asks you to analyze/check/review it, use <list_files> or <read_file>; do not merely say you are ready.
|
||||
4. Keep persona light. Do not introduce yourself unless the user greets you or asks who you are.`;
|
||||
|
||||
export function getSystemPrompt(): string {
|
||||
return BASE_SYSTEM_PROMPT;
|
||||
}
|
||||
|
||||
export const SYSTEM_PROMPT = BASE_SYSTEM_PROMPT;
|
||||
|
||||
Reference in New Issue
Block a user