diff --git a/package.json b/package.json index 02204f6..bd8ee26 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "g1nation", "displayName": "G1nation", "description": "High-performance autonomous local AI coding agent for VS Code. Features vectorized inference, asynchronous task management, and 100% offline processing.", - "version": "2.48.0", + "version": "2.50.0", "publisher": "connectailab", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/agent.ts b/src/agent.ts index a55edd9..172d45c 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -452,6 +452,12 @@ export class AgentExecutor { ].join('\n'); } } + if (prompt && recentProjectKnowledgeContext) { + assistantContent = this.ensureRecentProjectKnowledgeEvidence(assistantContent, recentProjectKnowledgeContext); + } + if (prompt && localPathContext) { + assistantContent = this.ensureLocalProjectPathEvidence(assistantContent, localPathContext); + } const traceMarkdown = secondBrainTrace ? renderSecondBrainTraceMarkdown(secondBrainTrace, !!options.secondBrainTraceDebug) : ''; @@ -824,6 +830,74 @@ export class AgentExecutor { } } + private ensureRecentProjectKnowledgeEvidence(content: string, recentProjectKnowledgeContext: string): string { + const recordPath = this.extractRecentProjectKnowledgeRecordPath(recentProjectKnowledgeContext); + if (!recordPath || content.includes(recordPath)) { + return content; + } + + const evidenceFiles = this.extractEvidenceFilesFromProjectKnowledge(recentProjectKnowledgeContext).slice(0, 8); + const evidenceSection = [ + '## 근거', + `이번 답변은 최근 생성된 프로젝트 지식 기록과 로컬 프로젝트 구조를 기준으로 작성했습니다: \`${recordPath}\``, + evidenceFiles.length + ? `확인된 근거 파일:\n${evidenceFiles.map((file) => `- \`${file}\``).join('\n')}` + : '' + ].filter(Boolean).join('\n\n'); + + return [ + content.trim(), + '', + evidenceSection + ].join('\n'); + } + + private ensureLocalProjectPathEvidence(content: string, localPathContext: string): string { + if (!localPathContext.includes('Access: succeeded') || content.includes('## 근거')) { + return content; + } + + const pathMatch = localPathContext.match(/Path:\s*(.+)/); + const projectPath = pathMatch?.[1]?.trim(); + const evidenceFiles = this.extractPriorityPreviewFiles(localPathContext).slice(0, 10); + if (!projectPath && evidenceFiles.length === 0) { + return content; + } + + const evidenceSection = [ + '## 근거', + projectPath + ? `이번 답변은 로컬 프로젝트 경로 \`${projectPath}\`에서 확인한 파일 구조와 코드 프리뷰를 기준으로 작성했습니다.` + : '이번 답변은 확인된 로컬 프로젝트 파일 구조와 코드 프리뷰를 기준으로 작성했습니다.', + evidenceFiles.length + ? `확인된 근거 파일:\n${evidenceFiles.map((file) => `- \`${file}\``).join('\n')}` + : '' + ].filter(Boolean).join('\n\n'); + + return [ + content.trim(), + '', + evidenceSection + ].join('\n'); + } + + private extractRecentProjectKnowledgeRecordPath(recentProjectKnowledgeContext: string): string | null { + return recentProjectKnowledgeContext.match(/project evidence:\s*(\/Volumes\/Data\/project\/Antigravity\/[^\s`"'<>]+\.md)/i)?.[1] || null; + } + + private extractEvidenceFilesFromProjectKnowledge(recentProjectKnowledgeContext: string): string[] { + const evidenceBlock = recentProjectKnowledgeContext.match(/## Evidence Files\n([\s\S]*?)(?=\n## |\n# |$)/i)?.[1] || ''; + const evidenceFiles = [...evidenceBlock.matchAll(/-\s+`([^`]+)`/g)].map((match) => match[1].trim()); + if (evidenceFiles.length > 0) { + return Array.from(new Set(evidenceFiles)); + } + + const structureBlock = recentProjectKnowledgeContext.match(/## Confirmed Structure\n([\s\S]*?)(?=\n## |\n# |$)/i)?.[1] || ''; + return Array.from(new Set([...structureBlock.matchAll(/`([^`]+)`/g)] + .map((match) => match[1].trim()) + .filter((value) => /[\\/]/.test(value) || /\.[a-z0-9]+$/i.test(value)))); + } + private findRecentProjectKnowledgeRecord(rootPath: string): string | null { const fromHistory = [...this.chatHistory] .reverse() diff --git a/src/sidebarProvider.ts b/src/sidebarProvider.ts index 7a8c0fc..b617b47 100644 --- a/src/sidebarProvider.ts +++ b/src/sidebarProvider.ts @@ -45,6 +45,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn private static readonly lastAgentStateKey = 'g1nation.lastAgentPath'; private static readonly chronicleProjectsStateKey = 'g1nation.chronicleProjects'; private static readonly activeChronicleProjectStateKey = 'g1nation.activeChronicleProjectId'; + private static readonly lastAutoChronicleSignatureStateKey = 'g1nation.lastAutoChronicleSignature'; private _view?: vscode.WebviewView; public brainEnabled = true; private _currentSessionBrainId: string | null = null; @@ -84,6 +85,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn case 'promptWithFile': await this._context.globalState.update(SidebarChatProvider.blankChatStateKey, false); await this._handlePrompt(data); + await this._autoWriteChronicleAfterPrompt(); // After prompt, save the session automatically await this._saveCurrentSession(); break; @@ -1480,6 +1482,136 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn } } + private async _autoWriteChronicleAfterPrompt() { + const profile = this._getActiveChronicleProject(); + if (!profile) return; + + const history = this._agent.getHistory(); + const latestUser = [...history].reverse().find(message => message.role === 'user')?.content || ''; + const latestAssistant = [...history].reverse().find(message => message.role === 'assistant')?.content || ''; + const recordType = this._inferAutoChronicleRecordType(latestUser, latestAssistant); + if (!recordType) return; + + const signature = [ + profile.projectId, + recordType, + this._summarizeTextForWiki(latestUser).slice(0, 240), + this._summarizeTextForWiki(latestAssistant).slice(0, 240) + ].join('|'); + const lastSignature = this._context.globalState.get(SidebarChatProvider.lastAutoChronicleSignatureStateKey, ''); + if (signature === lastSignature) return; + + try { + this._chronicle.ensureProject(profile); + const createdAt = new Date().toISOString(); + const title = this._summarizeForTitle(latestUser || latestAssistant || 'Project Chronicle Auto Record'); + const summary = this._summarizeTextForWiki(latestAssistant || latestUser); + let result; + + if (recordType === 'bug') { + const bugNumber = this._chronicle.nextBugNumber(profile); + result = this._chronicle.writeBug(profile, { + title, + symptom: this._summarizeTextForWiki(latestUser || 'Issue was detected during the conversation.'), + cause: 'Captured automatically from the current conversation. Confirm root cause during follow-up review if needed.', + fix: summary, + prevention: 'Keep automatic records tied to the active project and verify the relevant test or reproduction path.', + createdAt + }, bugNumber); + } else if (recordType === 'planning') { + result = this._chronicle.writePlanning(profile, { + featureName: title, + purpose: 'Capture the current planning or architecture direction before implementation continues.', + background: summary, + userIntent: this._summarizeTextForWiki(latestUser), + sourceRequest: latestUser || 'No user request captured in the current chat.', + scope: ['Continue from the active project conversation.', 'Use the selected project record folder automatically.'], + outOfScope: ['Manual record type selection.', 'Blocking the user with record-writing prompts.'], + developmentDirection: summary, + dependencyStrategy: 'Prefer existing project modules and local Markdown records.', + expectedValue: 'Future work can resume with the latest project intent and reasoning preserved.', + successCriteria: ['The record is saved automatically after a meaningful project turn.', 'The record stays under the active project.'], + developerInstruction: 'Use this record as lightweight context for the next development or review pass.', + createdAt + }); + } else if (recordType === 'decision') { + const adrNumber = this._chronicle.nextAdrNumber(profile); + result = this._chronicle.writeDecision(profile, { + title, + status: 'accepted', + context: this._summarizeTextForWiki(latestUser), + decision: summary, + reason: 'Captured automatically because the conversation contained decision-oriented language.', + alternatives: [], + consequences: ['Future prompts should treat this as project context unless the user changes direction.'], + createdAt + }, adrNumber); + } else if (recordType === 'development') { + result = this._chronicle.writeDevelopmentLog(profile, { + featureName: title, + purpose: 'Record the implementation or verification outcome from the current conversation.', + implementationSummary: summary, + architecture: 'Captured automatically from the assistant response and active project context.', + changedFiles: this._extractChangedFilesFromText(latestAssistant), + dependencyNotes: 'No new dependency note was captured automatically.', + bugs: [], + lessons: ['Automatic project records should be generated in the background when the turn contains durable project knowledge.'], + createdAt + }); + } else { + result = this._chronicle.writeDiscussion(profile, { + title, + userRequest: this._summarizeTextForWiki(latestUser || 'No user request captured in the current chat.'), + interpretedIntent: 'Capture a meaningful project discussion automatically instead of requiring manual record selection.', + questions: [], + discussions: [summary], + decisions: [], + createdAt + }); + } + + this._chronicle.appendTimeline(profile, [`Auto ${recordType} record created: ${result.relativePath}`], createdAt); + await this._context.globalState.update(SidebarChatProvider.lastAutoChronicleSignatureStateKey, signature); + await this._sendChronicleRecords(); + this.injectSystemMessage(`**[Chronicle Auto Saved]** ${recordType} · \`${result.filePath}\``); + } catch (err: any) { + logError('Automatic Chronicle record write failed.', { error: err?.message || String(err), recordType }); + } + } + + private _inferAutoChronicleRecordType(userText: string, assistantText: string): 'planning' | 'discussion' | 'decision' | 'development' | 'bug' | null { + const combined = `${userText}\n${assistantText}`; + if (!combined.trim()) return null; + if (/(기록하지마|저장하지마|no\s+record|do\s+not\s+record)/i.test(combined)) return null; + if (!/(프로젝트|코드|아키텍처|설계|개선|수정|구현|테스트|검증|이슈|문제|버그|오류|끊김|결정|방향|기록|지식|review|architecture|implement|fix|bug|issue|test|decision)/i.test(combined)) { + return null; + } + if (/(버그|오류|에러|이슈|문제|끊김|안\s*됨|실패|bug|error|issue|failed|failure)/i.test(userText)) { + return 'bug'; + } + if (/(수정 완료|개선 완료|구현 완료|패치|테스트.*통과|검증.*완료|변경.*파일|compile|jest|tsc|passed|implemented|fixed)/i.test(assistantText)) { + return 'development'; + } + if (/(결정|확정|채택|방향은|하기로|하지 않기로|decision|decide|accepted)/i.test(combined)) { + return 'decision'; + } + if (/(계획|설계|아키텍처|조사|방향|로드맵|mvp|planning|architecture|roadmap|design)/i.test(userText)) { + return 'planning'; + } + if (/(개선|수정|구현|테스트|검증|패킹|compile|jest|tsc|implement|fix|test|verify)/i.test(combined)) { + return 'development'; + } + return 'discussion'; + } + + private _extractChangedFilesFromText(text: string): string[] { + const files = new Set(); + for (const match of text.matchAll(/`([^`\n]+\.(?:ts|tsx|js|jsx|json|md|css|html|py|yml|yaml))`/gi)) { + files.add(match[1].trim()); + } + return files.size > 0 ? Array.from(files).slice(0, 12) : ['No explicit changed file list was captured automatically.']; + } + private async _writeChronicleRecord(recordType: string) { switch (recordType) { case 'planning': @@ -1891,6 +2023,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn width: 100%; align-items: center; } + .record-row { + grid-template-columns: auto minmax(0, 1fr) auto; + } .brand { font-weight: 700; font-size: 14px; color: var(--text-bright); letter-spacing: 0; display: flex; align-items: center; gap: 8px; min-width: 0; } .logo { width: 22px; height: 22px; background: var(--accent); color: #fff; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 900; } @@ -1917,6 +2052,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn box-shadow: 0 0 0 2px rgba(139, 148, 158, 0.12); flex-shrink: 0; } + .status-dot.ready { + background: #3fb950; + box-shadow: 0 0 0 2px rgba(63, 185, 80, 0.16); + } .chat { flex: 1; @@ -2420,22 +2559,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn -
-
- +
+
+ Auto Records
-
- -
-
-
@@ -2652,7 +2779,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn const attachPreview = document.getElementById('attachPreview'); const agentSel = document.getElementById('agentSel'); const designerSel = document.getElementById('designerSel'); - const chronicleRecordTypeSel = document.getElementById('chronicleRecordTypeSel'); const chronicleRecordSel = document.getElementById('chronicleRecordSel'); const editAgentBtn = document.getElementById('editAgentBtn'); const addAgentBtn = document.getElementById('addAgentBtn'); @@ -3228,10 +3354,6 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn document.getElementById('addDesignerBtn').onclick = () => vscode.postMessage({ type: 'createChronicleProject' }); document.getElementById('openDesignerBtn').onclick = () => vscode.postMessage({ type: 'openChronicleFolder' }); - document.getElementById('writeChronicleBtn').onclick = () => vscode.postMessage({ - type: 'writeChronicleRecord', - recordType: chronicleRecordTypeSel.value - }); document.getElementById('refreshChronicleRecordsBtn').onclick = () => vscode.postMessage({ type: 'getChronicleRecords' }); document.getElementById('openChronicleRecordBtn').onclick = () => { if (!chronicleRecordSel.value) return; diff --git a/tests/localPathPreflight.test.ts b/tests/localPathPreflight.test.ts index 340d66c..1e8380f 100644 --- a/tests/localPathPreflight.test.ts +++ b/tests/localPathPreflight.test.ts @@ -235,4 +235,68 @@ describe('local project path preflight', () => { expect(requestHistory[1].content).not.toContain('Datacollector'); expect(requestHistory[1].content).not.toContain('2nd Brain Trace'); }); + + it('adds visible evidence when answering from recent project knowledge context', () => { + const context: any = { + globalStorageUri: { fsPath: path.join(root, '.storage') }, + workspaceState: stateStore(), + globalState: stateStore() + }; + const agent = new AgentExecutor(context) as any; + const recordPath = '/Volumes/Data/project/Antigravity/ConnectAI/docs/records/ConnectAI/development/2026-05-02_connectai_project_knowledge_overview.md'; + const recentContext = [ + '[RECENT LOCAL PROJECT KNOWLEDGE]', + `Use this recently generated project knowledge record as project evidence: ${recordPath}`, + '', + '# ConnectAI Project Knowledge Overview', + '', + '## Evidence Files', + '- `package.json`', + '- `src/agent.ts`', + '- `src/sidebarProvider.ts`' + ].join('\n'); + const answer = '## 간단 요약\nConnectAI 아키텍처는 실행 흐름 분리를 중심으로 구성됩니다.'; + + const fixed = agent.ensureRecentProjectKnowledgeEvidence(answer, recentContext); + + expect(fixed).toContain('## 근거'); + expect(fixed).toContain(recordPath); + expect(fixed).toContain('`src/agent.ts`'); + expect(fixed).toContain('최근 생성된 프로젝트 지식 기록'); + }); + + it('adds visible evidence when answering from an explicit local project path', () => { + const context: any = { + globalStorageUri: { fsPath: path.join(root, '.storage') }, + workspaceState: stateStore(), + globalState: stateStore() + }; + const agent = new AgentExecutor(context) as any; + const localPathContext = [ + 'Path: /Volumes/Data/project/Antigravity/ConnectAI', + 'Access: succeeded', + 'Type: directory', + 'Scanned tree:', + 'package.json', + 'src/agent.ts', + 'Priority file previews:', + 'File: package.json', + '```json', + '{"name":"connectai"}', + '```', + 'File: src/agent.ts', + '```ts', + 'export class AgentExecutor {}', + '```' + ].join('\n'); + const answer = '## 간단 요약\nConnectAI 아키텍처는 실행 흐름 분리를 중심으로 구성됩니다.'; + + const fixed = agent.ensureLocalProjectPathEvidence(answer, localPathContext); + + expect(fixed).toContain('## 근거'); + expect(fixed).toContain('/Volumes/Data/project/Antigravity/ConnectAI'); + expect(fixed).toContain('`package.json`'); + expect(fixed).toContain('`src/agent.ts`'); + expect(fixed).toContain('로컬 프로젝트 경로'); + }); });