Version 2.50.0 Release: Autonomous Turn-Based Recording and UI Streamlining
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
+142
-20
@@ -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<string>(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<string>();
|
||||
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
|
||||
<button class="icon-btn" id="openDesignerBtn" data-tooltip="Open Record Folder">Open</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<div class="select-wrap">
|
||||
<select id="chronicleRecordTypeSel" title="Select Chronicle Record Type">
|
||||
<option value="planning">Planning</option>
|
||||
<option value="discussion">Discussion</option>
|
||||
<option value="decision">Decision</option>
|
||||
<option value="development">Development</option>
|
||||
<option value="bug">Bug</option>
|
||||
<option value="retrospective">Retrospective</option>
|
||||
</select>
|
||||
<div class="control-row record-row">
|
||||
<div class="status-pill" id="chronicleAutoStatus" title="Project records are saved automatically after meaningful project turns.">
|
||||
<span class="status-dot ready"></span><span>Auto Records</span>
|
||||
</div>
|
||||
<div class="tool-group" aria-label="Chronicle write actions">
|
||||
<button class="icon-btn" id="writeChronicleBtn" data-tooltip="Write Selected Record">Write</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<div class="select-wrap"><select id="chronicleRecordSel" title="Select Chronicle Record"></select></div>
|
||||
<div class="tool-group" aria-label="Chronicle record actions">
|
||||
<button class="icon-btn" id="refreshChronicleRecordsBtn" data-tooltip="Refresh Records">Ref</button>
|
||||
@@ -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;
|
||||
|
||||
@@ -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('로컬 프로젝트 경로');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user