Version 2.50.0 Release: Autonomous Turn-Based Recording and UI Streamlining

This commit is contained in:
g1nation
2026-05-03 10:07:34 +09:00
parent 70e46ab279
commit 5e3750a93e
4 changed files with 281 additions and 21 deletions
+1 -1
View File
@@ -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",
+74
View File
@@ -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
View File
@@ -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;
+64
View File
@@ -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('로컬 프로젝트 경로');
});
});