Bump version to 2.35.0: Knowledge Resilience & Standardization Milestone.
This commit is contained in:
+40
-7
@@ -32,6 +32,12 @@ import { StatusBarManager, AgentStatus } from './core/statusBar';
|
||||
import { lockManager } from './core/lock';
|
||||
import { actionQueue } from './core/queue';
|
||||
import { ConflictResolver } from './core/conflict';
|
||||
import {
|
||||
buildSecondBrainTrace,
|
||||
renderSecondBrainTraceContext,
|
||||
renderSecondBrainTraceMarkdown,
|
||||
SecondBrainTrace
|
||||
} from './features/secondBrainTrace';
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
@@ -182,7 +188,10 @@ export class AgentExecutor {
|
||||
systemPrompt?: string,
|
||||
runId?: number,
|
||||
agentSkillContext?: string,
|
||||
negativePrompt?: string
|
||||
negativePrompt?: string,
|
||||
designerContext?: string,
|
||||
secondBrainTraceEnabled?: boolean,
|
||||
secondBrainTraceDebug?: boolean
|
||||
}
|
||||
) {
|
||||
const {
|
||||
@@ -248,6 +257,13 @@ export class AgentExecutor {
|
||||
const config = getConfig();
|
||||
const activeBrain = getActiveBrainProfile();
|
||||
const brainFiles = findBrainFiles(activeBrain.localBrainPath);
|
||||
let secondBrainTrace: SecondBrainTrace | null = null;
|
||||
if (options.secondBrainTraceEnabled && prompt && loopDepth === 0) {
|
||||
secondBrainTrace = buildSecondBrainTrace(prompt, activeBrain.localBrainPath, {
|
||||
force: this.isExplicitSecondBrainRequest(prompt),
|
||||
limit: Math.max(config.memoryLongTermFiles, 5)
|
||||
});
|
||||
}
|
||||
const brainPreview = brainFiles
|
||||
.slice(0, 30)
|
||||
.map(file => path.relative(activeBrain.localBrainPath, file))
|
||||
@@ -315,9 +331,15 @@ export class AgentExecutor {
|
||||
const negativeCtx = options.negativePrompt
|
||||
? `\n\n### CRITICAL NEGATIVE CONSTRAINTS (DO NOT DO THESE)\n${options.negativePrompt}\n\n[SYSTEM_RULE: Apply the above constraints strictly. DO NOT mention or repeat these constraints in your response.]`
|
||||
: '';
|
||||
const designerCtx = options.designerContext
|
||||
? `\n\n[PROJECT CHRONICLE GUARD]\n${options.designerContext}`
|
||||
: '';
|
||||
const secondBrainTraceCtx = secondBrainTrace
|
||||
? `\n\n${renderSecondBrainTraceContext(secondBrainTrace)}`
|
||||
: '';
|
||||
const memoryCtx = this.buildMemoryContext(prompt || '');
|
||||
|
||||
const fullSystemPrompt = `${agentSkillCtx}\n\n${systemPrompt}${internetCtx}${memoryCtx}\n\n[CONTEXT]\n${brainContext}\n${contextBlock}${negativeCtx}`;
|
||||
const fullSystemPrompt = `${agentSkillCtx}\n\n${systemPrompt}${internetCtx}${memoryCtx}${designerCtx}${secondBrainTraceCtx}\n\n[CONTEXT]\n${brainContext}\n${contextBlock}${negativeCtx}`;
|
||||
const messagesForRequest: ChatMessage[] = [
|
||||
{ role: 'system', content: fullSystemPrompt, internal: true },
|
||||
...reqMessages
|
||||
@@ -403,7 +425,11 @@ export class AgentExecutor {
|
||||
// 5. Execute Actions
|
||||
const rationale = this.parseRationale(aiResponseText);
|
||||
const assistantContent = this.sanitizeAssistantContent(aiResponseText);
|
||||
const assistantMessage: ChatMessage = { role: 'assistant', content: assistantContent, internal: false, rationale };
|
||||
const traceMarkdown = secondBrainTrace
|
||||
? renderSecondBrainTraceMarkdown(secondBrainTrace, !!options.secondBrainTraceDebug)
|
||||
: '';
|
||||
const finalAssistantContent = traceMarkdown ? `${assistantContent}\n${traceMarkdown}` : assistantContent;
|
||||
const assistantMessage: ChatMessage = { role: 'assistant', content: finalAssistantContent, internal: false, rationale };
|
||||
this.chatHistory.push(assistantMessage);
|
||||
|
||||
this.statusBarManager.updateStatus(AgentStatus.Executing);
|
||||
@@ -426,9 +452,9 @@ export class AgentExecutor {
|
||||
if (report.length === 0 && loopDepth === 0 && this.isUnproductiveWaitingReply(assistantContent)) {
|
||||
assistantMessage.internal = false;
|
||||
const correctedReply = await this.buildUnproductiveReplyCorrection(prompt || '');
|
||||
assistantMessage.content = correctedReply;
|
||||
assistantMessage.content = traceMarkdown ? `${correctedReply}\n${traceMarkdown}` : correctedReply;
|
||||
this.emitHistoryChanged();
|
||||
this.webview.postMessage({ type: 'streamChunk', value: correctedReply });
|
||||
this.webview.postMessage({ type: 'streamChunk', value: assistantMessage.content });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -462,7 +488,7 @@ export class AgentExecutor {
|
||||
|
||||
this.emitHistoryChanged();
|
||||
this.statusBarManager.updateStatus(AgentStatus.Success);
|
||||
this.webview.postMessage({ type: 'streamChunk', value: assistantContent });
|
||||
this.webview.postMessage({ type: 'streamChunk', value: finalAssistantContent });
|
||||
|
||||
} catch (error: any) {
|
||||
this.statusBarManager.updateStatus(AgentStatus.Error, error.message);
|
||||
@@ -517,12 +543,15 @@ export class AgentExecutor {
|
||||
const selectedAgentContext = options.agentSkillContext
|
||||
? `\nSelected Agent Reference:\n${options.agentSkillContext}`
|
||||
: '';
|
||||
const designerContext = options.designerContext
|
||||
? `\nProject Chronicle Guard:\n${options.designerContext}`
|
||||
: '';
|
||||
|
||||
// 워크플로우 매니저에게 설정 기반 실행 위임
|
||||
const finalReport = await AgentWorkflowManager.runStrictWorkflow(
|
||||
prompt,
|
||||
modelName,
|
||||
`${brainContext}${selectedAgentContext}`,
|
||||
`${brainContext}${selectedAgentContext}${designerContext}`,
|
||||
signal,
|
||||
(step, msg) => {
|
||||
this.webview?.postMessage({ type: 'autoContinue', value: `${step}: ${msg}` });
|
||||
@@ -614,6 +643,10 @@ export class AgentExecutor {
|
||||
return mentionsBrain && asksOverview;
|
||||
}
|
||||
|
||||
private isExplicitSecondBrainRequest(prompt: string): boolean {
|
||||
return /(second brain|2nd brain|제2뇌|브레인|brain|기억|기록|노트|문서|참고해서|사용해서|검색해서|근거|출처)/i.test(prompt);
|
||||
}
|
||||
|
||||
private buildBrainOverviewReply(): string {
|
||||
const activeBrain = getActiveBrainProfile();
|
||||
const brainDir = activeBrain.localBrainPath;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ProjectProfile } from './types';
|
||||
|
||||
export function buildProjectChronicleGuardContext(project: ProjectProfile | null): string {
|
||||
const hasUsableProject = !!project?.recordRoot?.trim();
|
||||
const projectLines = project ? [
|
||||
`Project selection status: selected`,
|
||||
`Active record project: ${project.projectName}`,
|
||||
`Project root: ${project.projectRoot || 'Not set'}`,
|
||||
`Record root: ${project.recordRoot}`,
|
||||
`Core purpose: ${project.corePurpose || project.description || 'Not captured yet.'}`,
|
||||
`Record detail level: ${project.detailLevel}`
|
||||
] : [
|
||||
'Project selection status: not selected',
|
||||
'No active record project is selected. Before writing records, ask the user to select or create one.'
|
||||
];
|
||||
|
||||
return [
|
||||
...projectLines,
|
||||
'',
|
||||
'This guard is active for project ideas, feature requests, architecture proposals, implementation planning, and design decisions.',
|
||||
'',
|
||||
'Required response order for new ideas or feature requests:',
|
||||
'1. Request summary.',
|
||||
'2. Inferred user intent.',
|
||||
'3. Project record target check. If no project is selected, ask whether to use an existing project, create a new project, or skip recording.',
|
||||
'4. Record path check. If no record root is available, say a Markdown record path is required before writing files.',
|
||||
'5. Ask only 1 to 3 blocking questions.',
|
||||
'6. For every question, include "Question reason" explaining why it changes storage, scope, dependencies, or implementation direction.',
|
||||
'7. Direction review focused on project fit and dependency risk.',
|
||||
'8. Recommend a low-dependency MVP first.',
|
||||
'9. Put Vector DB, relational DB, knowledge graph, semantic search, and complex automation only under "Later expansion" unless the user explicitly asks for them now.',
|
||||
'10. End with "Candidate records for this discussion" and list planning, discussions, decisions, development, bugs, or retrospectives paths as candidates.',
|
||||
'',
|
||||
'Decision policy:',
|
||||
'- Do not mark a decision as accepted until the user confirms it.',
|
||||
'- Before confirmation, call decisions "candidates" or "pending".',
|
||||
'- Prefer "reduced adoption" when the idea is useful but too large for the MVP.',
|
||||
'',
|
||||
'Tone and scope:',
|
||||
'- Be practical and plain-spoken.',
|
||||
'- Avoid grand phrases like advanced cognitive architecture, compounding knowledge, perfect graph, or ultimate knowledge distiller.',
|
||||
'- When the user wants low dependency, keep the first proposal to Markdown, JSON, local files, and explicit user save actions.',
|
||||
'- Do not jump directly to large architectures. Narrow direction before expanding.',
|
||||
'',
|
||||
hasUsableProject
|
||||
? 'The current project and record root are available, so candidate record paths may use this project.'
|
||||
: 'The project or record root is missing, so candidate records must be described but not written.',
|
||||
'Do not claim a Markdown file was written unless a tool or explicit sidebar action actually wrote it.'
|
||||
].join('\n');
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { MarkdownFileWriter } from './markdownFileWriter';
|
||||
import {
|
||||
BugRecord,
|
||||
ChronicleRecordEntry,
|
||||
ChronicleWriteResult,
|
||||
DecisionRecord,
|
||||
DevelopmentLog,
|
||||
DiscussionRecord,
|
||||
PlanningDocument,
|
||||
ProjectProfile,
|
||||
RetrospectiveRecord
|
||||
} from './types';
|
||||
import {
|
||||
renderBugRecord,
|
||||
renderDecisionRecord,
|
||||
renderDevelopmentLog,
|
||||
renderDiscussionRecord,
|
||||
renderPlanningDocument,
|
||||
renderProjectProfile,
|
||||
renderProjectReadme,
|
||||
renderRetrospective,
|
||||
renderTimelineSeed
|
||||
} from './templates';
|
||||
|
||||
export * from './types';
|
||||
export * from './guardPrompt';
|
||||
|
||||
const sectionDirs = ['planning', 'discussions', 'decisions', 'development', 'bugs', 'retrospectives'];
|
||||
|
||||
export class ProjectChronicleManager {
|
||||
private readonly writer = new MarkdownFileWriter();
|
||||
|
||||
public ensureProject(profile: ProjectProfile): void {
|
||||
if (!profile.recordRoot || !profile.recordRoot.trim()) {
|
||||
throw new Error('Record root is required before writing chronicle documents.');
|
||||
}
|
||||
|
||||
this.writer.ensureDir(profile.recordRoot);
|
||||
for (const dir of sectionDirs) {
|
||||
this.writer.ensureDir(path.join(profile.recordRoot, dir));
|
||||
}
|
||||
|
||||
this.writeSeedFile(path.join(profile.recordRoot, 'README.md'), renderProjectReadme(profile));
|
||||
this.writeSeedFile(path.join(profile.recordRoot, 'project-profile.md'), renderProjectProfile(profile));
|
||||
this.writeSeedFile(path.join(profile.recordRoot, 'timeline.md'), renderTimelineSeed(profile));
|
||||
this.writeProjectConfig(profile);
|
||||
}
|
||||
|
||||
public writePlanning(profile: ProjectProfile, doc: PlanningDocument): ChronicleWriteResult {
|
||||
this.ensureProject(profile);
|
||||
const date = this.datePart(doc.createdAt);
|
||||
const fileName = `${date}_${this.slug(doc.featureName)}.md`;
|
||||
return this.write(profile, 'planning', fileName, renderPlanningDocument(doc));
|
||||
}
|
||||
|
||||
public writeDiscussion(profile: ProjectProfile, record: DiscussionRecord): ChronicleWriteResult {
|
||||
this.ensureProject(profile);
|
||||
const date = this.datePart(record.createdAt);
|
||||
const fileName = `${date}_${this.slug(record.title)}.md`;
|
||||
return this.write(profile, 'discussions', fileName, renderDiscussionRecord(record));
|
||||
}
|
||||
|
||||
public writeDecision(profile: ProjectProfile, record: DecisionRecord, adrNumber: number): ChronicleWriteResult {
|
||||
this.ensureProject(profile);
|
||||
const padded = String(adrNumber).padStart(4, '0');
|
||||
const fileName = `ADR-${padded}-${this.slug(record.title)}.md`;
|
||||
return this.write(profile, 'decisions', fileName, renderDecisionRecord(record));
|
||||
}
|
||||
|
||||
public writeDevelopmentLog(profile: ProjectProfile, log: DevelopmentLog): ChronicleWriteResult {
|
||||
this.ensureProject(profile);
|
||||
const date = this.datePart(log.createdAt);
|
||||
const fileName = `${date}_${this.slug(log.featureName)}_implementation.md`;
|
||||
return this.write(profile, 'development', fileName, renderDevelopmentLog(log));
|
||||
}
|
||||
|
||||
public writeBug(profile: ProjectProfile, record: BugRecord, bugNumber: number): ChronicleWriteResult {
|
||||
this.ensureProject(profile);
|
||||
const padded = String(bugNumber).padStart(4, '0');
|
||||
const fileName = `BUG-${padded}-${this.slug(record.title)}.md`;
|
||||
return this.write(profile, 'bugs', fileName, renderBugRecord(record));
|
||||
}
|
||||
|
||||
public writeRetrospective(profile: ProjectProfile, record: RetrospectiveRecord): ChronicleWriteResult {
|
||||
this.ensureProject(profile);
|
||||
const date = this.datePart(record.createdAt);
|
||||
const fileName = `${date}_${this.slug(record.title)}.md`;
|
||||
return this.write(profile, 'retrospectives', fileName, renderRetrospective(record));
|
||||
}
|
||||
|
||||
public appendTimeline(profile: ProjectProfile, lines: string[], createdAt: string = new Date().toISOString()): void {
|
||||
this.ensureProject(profile);
|
||||
const timelinePath = path.join(profile.recordRoot, 'timeline.md');
|
||||
const markdown = [
|
||||
'',
|
||||
`## ${this.datePart(createdAt)}`,
|
||||
...lines.map(line => `- ${line}`)
|
||||
].join('\n');
|
||||
this.writer.appendMarkdown(timelinePath, markdown);
|
||||
}
|
||||
|
||||
public nextAdrNumber(profile: ProjectProfile): number {
|
||||
return this.nextNumber(path.join(profile.recordRoot, 'decisions'), /^ADR-(\d+)/);
|
||||
}
|
||||
|
||||
public nextBugNumber(profile: ProjectProfile): number {
|
||||
return this.nextNumber(path.join(profile.recordRoot, 'bugs'), /^BUG-(\d+)/);
|
||||
}
|
||||
|
||||
public listRecords(profile: ProjectProfile): ChronicleRecordEntry[] {
|
||||
this.ensureProject(profile);
|
||||
const records: ChronicleRecordEntry[] = [];
|
||||
|
||||
for (const section of sectionDirs) {
|
||||
const sectionPath = path.join(profile.recordRoot, section);
|
||||
if (!fs.existsSync(sectionPath)) continue;
|
||||
|
||||
for (const fileName of fs.readdirSync(sectionPath)) {
|
||||
if (!fileName.endsWith('.md')) continue;
|
||||
const filePath = path.join(sectionPath, fileName);
|
||||
const stat = fs.statSync(filePath);
|
||||
records.push({
|
||||
section,
|
||||
fileName,
|
||||
filePath,
|
||||
relativePath: path.relative(profile.recordRoot, filePath),
|
||||
updatedAt: stat.mtimeMs
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return records.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
}
|
||||
|
||||
private write(profile: ProjectProfile, section: string, fileName: string, markdown: string): ChronicleWriteResult {
|
||||
const filePath = this.writer.writeMarkdown(path.join(profile.recordRoot, section, fileName), markdown);
|
||||
return {
|
||||
filePath,
|
||||
relativePath: path.relative(profile.recordRoot, filePath)
|
||||
};
|
||||
}
|
||||
|
||||
private writeSeedFile(filePath: string, content: string): void {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
this.writer.writeMarkdown(filePath, content);
|
||||
}
|
||||
}
|
||||
|
||||
private writeProjectConfig(profile: ProjectProfile): void {
|
||||
const configPath = path.join(profile.recordRoot, 'chronicle.config.json');
|
||||
const config = {
|
||||
projectId: profile.projectId,
|
||||
projectName: profile.projectName,
|
||||
projectRoot: profile.projectRoot || '',
|
||||
recordRoot: profile.recordRoot,
|
||||
description: profile.description || '',
|
||||
corePurpose: profile.corePurpose || '',
|
||||
detailLevel: profile.detailLevel,
|
||||
createdAt: profile.createdAt,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
private nextNumber(dir: string, pattern: RegExp): number {
|
||||
if (!fs.existsSync(dir)) return 1;
|
||||
const max = fs.readdirSync(dir).reduce((current, fileName) => {
|
||||
const match = fileName.match(pattern);
|
||||
if (!match) return current;
|
||||
return Math.max(current, Number(match[1]) || 0);
|
||||
}, 0);
|
||||
return max + 1;
|
||||
}
|
||||
|
||||
private slug(value: string): string {
|
||||
const slug = value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9가-힣]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
.slice(0, 60);
|
||||
return slug || 'record';
|
||||
}
|
||||
|
||||
private datePart(iso?: string): string {
|
||||
return (iso || new Date().toISOString()).slice(0, 10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export class MarkdownFileWriter {
|
||||
public ensureDir(dirPath: string): void {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
public writeMarkdown(filePath: string, content: string): string {
|
||||
this.ensureDir(path.dirname(filePath));
|
||||
const finalPath = this.getAvailablePath(filePath);
|
||||
fs.writeFileSync(finalPath, this.normalize(content), 'utf8');
|
||||
return finalPath;
|
||||
}
|
||||
|
||||
public appendMarkdown(filePath: string, content: string): void {
|
||||
this.ensureDir(path.dirname(filePath));
|
||||
fs.appendFileSync(filePath, this.normalize(content), 'utf8');
|
||||
}
|
||||
|
||||
private getAvailablePath(filePath: string): string {
|
||||
if (!fs.existsSync(filePath)) return filePath;
|
||||
|
||||
const dir = path.dirname(filePath);
|
||||
const ext = path.extname(filePath);
|
||||
const base = path.basename(filePath, ext);
|
||||
let index = 2;
|
||||
|
||||
while (true) {
|
||||
const candidate = path.join(dir, `${base}-${index}${ext}`);
|
||||
if (!fs.existsSync(candidate)) return candidate;
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
private normalize(content: string): string {
|
||||
return content.replace(/\r\n/g, '\n').trimEnd() + '\n';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import {
|
||||
BugRecord,
|
||||
DecisionRecord,
|
||||
DevelopmentLog,
|
||||
DiscussionRecord,
|
||||
PlanningDocument,
|
||||
ProjectProfile,
|
||||
RetrospectiveRecord
|
||||
} from './types';
|
||||
|
||||
const list = (items: string[] | undefined, fallback: string = 'Not captured yet.') => {
|
||||
if (!items || items.length === 0) return fallback;
|
||||
return items.map(item => `- ${item}`).join('\n');
|
||||
};
|
||||
|
||||
const dateOnly = (iso?: string) => (iso || new Date().toISOString()).slice(0, 10);
|
||||
|
||||
export function renderProjectReadme(profile: ProjectProfile): string {
|
||||
return [
|
||||
`# ${profile.projectName} Chronicle Records`,
|
||||
'',
|
||||
'## Project',
|
||||
`- ID: ${profile.projectId}`,
|
||||
`- Root: ${profile.projectRoot || 'Not set'}`,
|
||||
`- Record root: ${profile.recordRoot}`,
|
||||
`- Detail level: ${profile.detailLevel}`,
|
||||
'',
|
||||
'## Purpose',
|
||||
profile.corePurpose || profile.description || 'Not captured yet.',
|
||||
'',
|
||||
'## Folders',
|
||||
'- `planning/`',
|
||||
'- `discussions/`',
|
||||
'- `decisions/`',
|
||||
'- `development/`',
|
||||
'- `bugs/`',
|
||||
'- `retrospectives/`'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function renderProjectProfile(profile: ProjectProfile): string {
|
||||
return [
|
||||
'# Project Profile',
|
||||
'',
|
||||
'## Project Name',
|
||||
profile.projectName,
|
||||
'',
|
||||
'## Description',
|
||||
profile.description || 'Not captured yet.',
|
||||
'',
|
||||
'## Project Root',
|
||||
profile.projectRoot || 'Not set',
|
||||
'',
|
||||
'## Record Root',
|
||||
profile.recordRoot,
|
||||
'',
|
||||
'## Core Purpose',
|
||||
profile.corePurpose || 'Not captured yet.',
|
||||
'',
|
||||
'## Target Users',
|
||||
list(profile.targetUsers),
|
||||
'',
|
||||
'## Avoid Directions',
|
||||
list(profile.avoidDirections),
|
||||
'',
|
||||
'## Record Detail Level',
|
||||
profile.detailLevel,
|
||||
'',
|
||||
'## Created',
|
||||
profile.createdAt,
|
||||
'',
|
||||
'## Updated',
|
||||
profile.updatedAt
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function renderTimelineSeed(profile: ProjectProfile): string {
|
||||
return [
|
||||
'# Project Timeline',
|
||||
'',
|
||||
`## ${dateOnly(profile.createdAt)}`,
|
||||
`- Project Chronicle record folder initialized for ${profile.projectName}.`
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function renderPlanningDocument(doc: PlanningDocument): string {
|
||||
return [
|
||||
`# Feature Plan: ${doc.featureName}`,
|
||||
'',
|
||||
'## 1. Feature Name',
|
||||
doc.featureName,
|
||||
'',
|
||||
'## 2. Reason',
|
||||
doc.purpose,
|
||||
'',
|
||||
'## 3. Original User Request',
|
||||
doc.sourceRequest || 'Not captured yet.',
|
||||
'',
|
||||
'## 4. Interpreted User Intent',
|
||||
doc.userIntent,
|
||||
'',
|
||||
'## 5. Background',
|
||||
doc.background,
|
||||
'',
|
||||
'## 6. Scope',
|
||||
list(doc.scope),
|
||||
'',
|
||||
'## 7. Out Of Scope',
|
||||
list(doc.outOfScope),
|
||||
'',
|
||||
'## 8. Development Direction',
|
||||
doc.developmentDirection,
|
||||
'',
|
||||
'## 9. Dependency Strategy',
|
||||
doc.dependencyStrategy,
|
||||
'',
|
||||
'## 10. Expected Value',
|
||||
doc.expectedValue,
|
||||
'',
|
||||
'## 11. Success Criteria',
|
||||
list(doc.successCriteria),
|
||||
'',
|
||||
'## 12. Developer Instruction',
|
||||
doc.developerInstruction
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function renderDiscussionRecord(record: DiscussionRecord): string {
|
||||
const questions = record.questions.length
|
||||
? record.questions.map((q, index) => [
|
||||
`### Question ${index + 1}`,
|
||||
q.question,
|
||||
'',
|
||||
'### Reason',
|
||||
q.reason,
|
||||
'',
|
||||
'### Expected Information',
|
||||
q.expectedInformation,
|
||||
'',
|
||||
'### Impact On Decision',
|
||||
q.impactOnDecision,
|
||||
'',
|
||||
q.userAnswer ? `### User Answer\n${q.userAnswer}` : '',
|
||||
q.result ? `### Result\n${q.result}` : ''
|
||||
].filter(Boolean).join('\n')).join('\n\n')
|
||||
: 'No explicit question was captured.';
|
||||
|
||||
return [
|
||||
`# Discussion: ${record.title}`,
|
||||
'',
|
||||
'## User Request Summary',
|
||||
record.userRequest,
|
||||
'',
|
||||
'## Interpreted Intent',
|
||||
record.interpretedIntent,
|
||||
'',
|
||||
'## Questions',
|
||||
questions,
|
||||
'',
|
||||
'## Main Discussion',
|
||||
list(record.discussions),
|
||||
'',
|
||||
'## Decisions',
|
||||
record.decisions.length
|
||||
? record.decisions.map(decision => `- ${decision.title}: ${decision.decision}`).join('\n')
|
||||
: 'No decisions captured yet.'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function renderDecisionRecord(record: DecisionRecord): string {
|
||||
return [
|
||||
`# ADR: ${record.title}`,
|
||||
'',
|
||||
'## Status',
|
||||
record.status,
|
||||
'',
|
||||
'## Context',
|
||||
record.context,
|
||||
'',
|
||||
'## Decision',
|
||||
record.decision,
|
||||
'',
|
||||
'## Reason',
|
||||
record.reason,
|
||||
'',
|
||||
'## Alternatives',
|
||||
list(record.alternatives),
|
||||
'',
|
||||
'## Consequences',
|
||||
list(record.consequences)
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function renderDevelopmentLog(log: DevelopmentLog): string {
|
||||
return [
|
||||
`# Development Log: ${log.featureName}`,
|
||||
'',
|
||||
'## Purpose',
|
||||
log.purpose,
|
||||
'',
|
||||
'## Implementation Summary',
|
||||
log.implementationSummary,
|
||||
'',
|
||||
'## Architecture',
|
||||
log.architecture,
|
||||
'',
|
||||
'## Changed Files',
|
||||
list(log.changedFiles),
|
||||
'',
|
||||
'## Dependency Notes',
|
||||
log.dependencyNotes,
|
||||
'',
|
||||
'## Bugs',
|
||||
log.bugs.length ? log.bugs.map(bug => `- ${bug.title}: ${bug.symptom}`).join('\n') : 'No bugs recorded.',
|
||||
'',
|
||||
'## Lessons',
|
||||
list(log.lessons)
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function renderBugRecord(record: BugRecord): string {
|
||||
return [
|
||||
`# Bug: ${record.title}`,
|
||||
'',
|
||||
'## Date',
|
||||
dateOnly(record.createdAt),
|
||||
'',
|
||||
'## Symptom',
|
||||
record.symptom,
|
||||
'',
|
||||
'## Cause',
|
||||
record.cause,
|
||||
'',
|
||||
'## Fix',
|
||||
record.fix,
|
||||
'',
|
||||
'## Prevention',
|
||||
record.prevention
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function renderRetrospective(record: RetrospectiveRecord): string {
|
||||
return [
|
||||
`# Retrospective: ${record.title}`,
|
||||
'',
|
||||
'## Summary',
|
||||
record.summary,
|
||||
'',
|
||||
'## Went Well',
|
||||
list(record.wentWell),
|
||||
'',
|
||||
'## To Improve',
|
||||
list(record.toImprove),
|
||||
'',
|
||||
'## Next Actions',
|
||||
list(record.nextActions)
|
||||
].join('\n');
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
export type ChronicleDetailLevel = 'simple' | 'standard' | 'detailed';
|
||||
|
||||
export interface ProjectProfile {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
projectRoot?: string;
|
||||
recordRoot: string;
|
||||
description?: string;
|
||||
corePurpose?: string;
|
||||
targetUsers?: string[];
|
||||
avoidDirections?: string[];
|
||||
detailLevel: ChronicleDetailLevel;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface QuestionRecord {
|
||||
question: string;
|
||||
reason: string;
|
||||
expectedInformation: string;
|
||||
impactOnDecision: string;
|
||||
userAnswer?: string;
|
||||
result?: string;
|
||||
}
|
||||
|
||||
export interface DecisionRecord {
|
||||
title: string;
|
||||
status: 'accepted' | 'rejected' | 'deferred' | 'changed';
|
||||
context: string;
|
||||
decision: string;
|
||||
reason: string;
|
||||
alternatives?: string[];
|
||||
consequences?: string[];
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface PlanningDocument {
|
||||
featureName: string;
|
||||
purpose: string;
|
||||
background: string;
|
||||
userIntent: string;
|
||||
scope: string[];
|
||||
outOfScope: string[];
|
||||
developmentDirection: string;
|
||||
dependencyStrategy: string;
|
||||
expectedValue: string;
|
||||
successCriteria: string[];
|
||||
developerInstruction: string;
|
||||
sourceRequest?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface DiscussionRecord {
|
||||
title: string;
|
||||
userRequest: string;
|
||||
interpretedIntent: string;
|
||||
questions: QuestionRecord[];
|
||||
discussions: string[];
|
||||
decisions: DecisionRecord[];
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface DevelopmentLog {
|
||||
featureName: string;
|
||||
purpose: string;
|
||||
implementationSummary: string;
|
||||
architecture: string;
|
||||
changedFiles: string[];
|
||||
dependencyNotes: string;
|
||||
bugs: BugRecord[];
|
||||
lessons: string[];
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface BugRecord {
|
||||
title: string;
|
||||
symptom: string;
|
||||
cause: string;
|
||||
fix: string;
|
||||
prevention: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface RetrospectiveRecord {
|
||||
title: string;
|
||||
summary: string;
|
||||
wentWell: string[];
|
||||
toImprove: string[];
|
||||
nextActions: string[];
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface ChronicleWriteResult {
|
||||
filePath: string;
|
||||
relativePath: string;
|
||||
}
|
||||
|
||||
export interface ChronicleRecordEntry {
|
||||
section: string;
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
relativePath: string;
|
||||
updatedAt: number;
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { findBrainFiles, summarizeText } from '../utils';
|
||||
|
||||
export interface SecondBrainTraceDocument {
|
||||
title: string;
|
||||
path: string;
|
||||
absolutePath: string;
|
||||
score: number;
|
||||
excerpt: string;
|
||||
usedInAnswer: boolean;
|
||||
usedFor?: string;
|
||||
excludedReason?: string;
|
||||
}
|
||||
|
||||
export interface SecondBrainTrace {
|
||||
userQuery: string;
|
||||
shouldUseSecondBrain: boolean;
|
||||
secondBrainUsed: boolean;
|
||||
reason: string;
|
||||
retrievalQuery: string;
|
||||
searchedCollections: string[];
|
||||
retrievedDocuments: SecondBrainTraceDocument[];
|
||||
groundingScore: number;
|
||||
}
|
||||
|
||||
export function buildSecondBrainTrace(userQuery: string, brainRoot: string, options: {
|
||||
force?: boolean;
|
||||
limit?: number;
|
||||
} = {}): SecondBrainTrace {
|
||||
const query = userQuery.trim();
|
||||
const shouldUseSecondBrain = !!options.force || shouldUseBrain(query);
|
||||
const retrievalQuery = buildRetrievalQuery(query);
|
||||
const baseTrace: SecondBrainTrace = {
|
||||
userQuery: query,
|
||||
shouldUseSecondBrain,
|
||||
secondBrainUsed: false,
|
||||
reason: shouldUseSecondBrain
|
||||
? 'Project-specific or memory-sensitive information may be needed.'
|
||||
: 'This looks answerable without project-specific Second Brain context.',
|
||||
retrievalQuery,
|
||||
searchedCollections: [],
|
||||
retrievedDocuments: [],
|
||||
groundingScore: 0
|
||||
};
|
||||
|
||||
if (!shouldUseSecondBrain) return baseTrace;
|
||||
if (!brainRoot || !fs.existsSync(brainRoot)) {
|
||||
return {
|
||||
...baseTrace,
|
||||
reason: 'Second Brain was requested, but the active brain folder does not exist.'
|
||||
};
|
||||
}
|
||||
|
||||
const files = findBrainFiles(brainRoot);
|
||||
const terms = tokenize(retrievalQuery);
|
||||
const scored = files.map((file) => scoreFile(file, brainRoot, terms))
|
||||
.filter((doc) => doc.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, options.limit || 5);
|
||||
|
||||
const usedDocs = scored.slice(0, Math.min(3, scored.length)).map((doc) => ({
|
||||
...doc,
|
||||
usedInAnswer: true,
|
||||
usedFor: inferUsedFor(doc.excerpt)
|
||||
}));
|
||||
const unusedDocs = scored.slice(usedDocs.length).map((doc) => ({
|
||||
...doc,
|
||||
usedInAnswer: false,
|
||||
excludedReason: 'Lower relevance than the documents selected as answer context.'
|
||||
}));
|
||||
const retrievedDocuments = [...usedDocs, ...unusedDocs];
|
||||
const usedCount = retrievedDocuments.filter((doc) => doc.usedInAnswer).length;
|
||||
|
||||
return {
|
||||
...baseTrace,
|
||||
secondBrainUsed: retrievedDocuments.length > 0,
|
||||
reason: retrievedDocuments.length > 0
|
||||
? 'Relevant Markdown notes were found and selected as answer context.'
|
||||
: 'Second Brain search ran, but no sufficiently relevant Markdown notes were found.',
|
||||
searchedCollections: inferCollections(retrievedDocuments),
|
||||
retrievedDocuments,
|
||||
groundingScore: retrievedDocuments.length === 0
|
||||
? 0
|
||||
: Number((usedCount / retrievedDocuments.length).toFixed(2))
|
||||
};
|
||||
}
|
||||
|
||||
export function renderSecondBrainTraceContext(trace: SecondBrainTrace): string {
|
||||
if (!trace.shouldUseSecondBrain) {
|
||||
return [
|
||||
'[SECOND BRAIN TRACE]',
|
||||
'Second Brain was not used for this request.',
|
||||
`Reason: ${trace.reason}`,
|
||||
'If the user explicitly asks to use Second Brain or asks project-specific memory questions, use it.'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const docs = trace.retrievedDocuments
|
||||
.filter((doc) => doc.usedInAnswer)
|
||||
.map((doc) => [
|
||||
`- ${doc.path}`,
|
||||
` Score: ${doc.score}`,
|
||||
` Relevant content: ${doc.excerpt}`
|
||||
].join('\n'))
|
||||
.join('\n');
|
||||
|
||||
return [
|
||||
'[SECOND BRAIN TRACE]',
|
||||
`Second Brain used: ${trace.secondBrainUsed ? 'yes' : 'no'}`,
|
||||
`Retrieval query: ${trace.retrievalQuery}`,
|
||||
`Reason: ${trace.reason}`,
|
||||
docs ? `Selected notes:\n${docs}` : 'Selected notes: none',
|
||||
'',
|
||||
'When answering, use only selected notes that are relevant. If these notes influence the answer, mention them in the final reference section.'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function renderSecondBrainTraceMarkdown(trace: SecondBrainTrace, debug: boolean = false): string {
|
||||
const usedDocs = trace.retrievedDocuments.filter((doc) => doc.usedInAnswer);
|
||||
const unusedDocs = trace.retrievedDocuments.filter((doc) => !doc.usedInAnswer);
|
||||
const usedText = usedDocs.length
|
||||
? usedDocs.map((doc) => [
|
||||
`- \`${doc.path}\``,
|
||||
` - Score: ${doc.score}`,
|
||||
` - 참고 내용: ${doc.excerpt}`
|
||||
].join('\n')).join('\n')
|
||||
: '- 없음';
|
||||
const unusedText = unusedDocs.length
|
||||
? unusedDocs.map((doc) => [
|
||||
`- \`${doc.path}\``,
|
||||
` - 제외 이유: ${doc.excludedReason || '이번 답변의 핵심 근거로 선택되지 않았습니다.'}`
|
||||
].join('\n')).join('\n')
|
||||
: '- 없음';
|
||||
|
||||
const sections = [
|
||||
'',
|
||||
'## 2nd Brain 사용 여부',
|
||||
trace.secondBrainUsed ? '사용함' : '사용하지 않음',
|
||||
'',
|
||||
'## 이유',
|
||||
trace.reason,
|
||||
'',
|
||||
'## 참고한 2nd Brain 문서',
|
||||
usedText,
|
||||
'',
|
||||
'## 검색했지만 사용하지 않은 문서',
|
||||
unusedText,
|
||||
'',
|
||||
'## 참고 품질',
|
||||
`- 검색된 노트: ${trace.retrievedDocuments.length}개`,
|
||||
`- 실제 사용된 노트: ${usedDocs.length}개`,
|
||||
`- 답변 근거도: ${trace.groundingScore}`
|
||||
];
|
||||
|
||||
if (debug) {
|
||||
sections.push(
|
||||
'',
|
||||
'## Second Brain Debug JSON',
|
||||
'```json',
|
||||
JSON.stringify({
|
||||
secondBrainUsed: trace.secondBrainUsed,
|
||||
shouldUseSecondBrain: trace.shouldUseSecondBrain,
|
||||
retrievalQuery: trace.retrievalQuery,
|
||||
searchedCollections: trace.searchedCollections,
|
||||
retrievedDocuments: trace.retrievedDocuments.map((doc) => ({
|
||||
path: doc.path,
|
||||
score: doc.score,
|
||||
usedInAnswer: doc.usedInAnswer,
|
||||
usedFor: doc.usedFor,
|
||||
excludedReason: doc.excludedReason
|
||||
})),
|
||||
groundingScore: trace.groundingScore
|
||||
}, null, 2),
|
||||
'```'
|
||||
);
|
||||
}
|
||||
|
||||
return sections.join('\n');
|
||||
}
|
||||
|
||||
function shouldUseBrain(query: string): boolean {
|
||||
const normalized = query.toLowerCase();
|
||||
return /(second brain|2nd brain|제2뇌|브레인|brain|기억|기록|문서|노트|내가|우리|프로젝트|결정|adr|chronicle|가드|설계 원칙|mvp|제외|왜)/i.test(normalized);
|
||||
}
|
||||
|
||||
function buildRetrievalQuery(query: string): string {
|
||||
return tokenize(query).slice(0, 16).join(' ');
|
||||
}
|
||||
|
||||
function tokenize(value: string): string[] {
|
||||
const stopWords = new Set(['그리고', '그런데', '해서', '하는', '있어', 'what', 'how', 'the', 'and', 'for', 'with']);
|
||||
return value
|
||||
.toLowerCase()
|
||||
.split(/[^a-z0-9가-힣_]+/g)
|
||||
.map((term) => term.trim())
|
||||
.filter((term) => term.length >= 2 && !stopWords.has(term));
|
||||
}
|
||||
|
||||
function scoreFile(file: string, brainRoot: string, terms: string[]): SecondBrainTraceDocument {
|
||||
const relative = path.relative(brainRoot, file);
|
||||
const title = path.basename(file, path.extname(file));
|
||||
const basename = relative.toLowerCase();
|
||||
let content = '';
|
||||
try {
|
||||
content = fs.readFileSync(file, 'utf8');
|
||||
} catch {
|
||||
content = '';
|
||||
}
|
||||
|
||||
const lower = content.toLowerCase();
|
||||
let score = 0;
|
||||
for (const term of terms) {
|
||||
if (basename.includes(term)) score += 4;
|
||||
const matches = lower.split(term).length - 1;
|
||||
if (matches > 0) score += Math.min(matches, 6);
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
path: relative,
|
||||
absolutePath: file,
|
||||
score: Number((score / Math.max(terms.length, 1)).toFixed(2)),
|
||||
excerpt: summarizeText(bestExcerpt(content, terms), 420),
|
||||
usedInAnswer: false
|
||||
};
|
||||
}
|
||||
|
||||
function bestExcerpt(content: string, terms: string[]): string {
|
||||
const paragraphs = content
|
||||
.split(/\n\s*\n/g)
|
||||
.map((part) => part.replace(/\s+/g, ' ').trim())
|
||||
.filter(Boolean);
|
||||
if (paragraphs.length === 0) return '';
|
||||
|
||||
let best = paragraphs[0];
|
||||
let bestScore = -1;
|
||||
for (const paragraph of paragraphs) {
|
||||
const lower = paragraph.toLowerCase();
|
||||
const score = terms.reduce((sum, term) => sum + (lower.includes(term) ? 1 : 0), 0);
|
||||
if (score > bestScore) {
|
||||
best = paragraph;
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function inferCollections(docs: SecondBrainTraceDocument[]): string[] {
|
||||
const collections = new Set<string>();
|
||||
for (const doc of docs) {
|
||||
const first = doc.path.split(/[\\/]/)[0];
|
||||
if (first) collections.add(first);
|
||||
}
|
||||
return Array.from(collections);
|
||||
}
|
||||
|
||||
function inferUsedFor(excerpt: string): string {
|
||||
if (/의존|coupl|독립|분리/i.test(excerpt)) return '의존도와 독립 모듈 판단';
|
||||
if (/markdown|마크다운/i.test(excerpt)) return 'Markdown 기반 저장 방향';
|
||||
if (/질문|의도|reason/i.test(excerpt)) return '질문 의도와 기록 방식';
|
||||
if (/mvp|제외|scope/i.test(excerpt)) return 'MVP 범위 판단';
|
||||
return '프로젝트 고유 맥락 확인';
|
||||
}
|
||||
+761
-4
@@ -15,6 +15,7 @@ import {
|
||||
import { getConfig } from './config';
|
||||
import { AgentExecutor, ChatMessage } from './agent';
|
||||
import { BridgeInterface } from './bridge';
|
||||
import { buildProjectChronicleGuardContext, ProjectChronicleManager, ProjectProfile } from './features/projectChronicle';
|
||||
|
||||
interface LastVisibleChatSnapshot {
|
||||
history: ChatMessage[];
|
||||
@@ -42,10 +43,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
private static readonly lastVisibleChatStateKey = 'g1nation.lastVisibleChat';
|
||||
private static readonly blankChatStateKey = 'g1nation.blankChatActive';
|
||||
private static readonly lastAgentStateKey = 'g1nation.lastAgentPath';
|
||||
private static readonly chronicleProjectsStateKey = 'g1nation.chronicleProjects';
|
||||
private static readonly activeChronicleProjectStateKey = 'g1nation.activeChronicleProjectId';
|
||||
private _view?: vscode.WebviewView;
|
||||
public brainEnabled = true;
|
||||
private _currentSessionBrainId: string | null = null;
|
||||
private _currentNegativePrompt: string = '';
|
||||
private readonly _chronicle = new ProjectChronicleManager();
|
||||
|
||||
constructor(
|
||||
private readonly _extensionUri: vscode.Uri,
|
||||
@@ -89,6 +93,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
await this._sendSessionList();
|
||||
await this._sendModels();
|
||||
await this._sendConfig();
|
||||
await this._sendChronicleProjects();
|
||||
await this._restoreActiveSessionIntoView();
|
||||
break;
|
||||
case 'toggleMultiAgent':
|
||||
@@ -103,6 +108,45 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
case 'getAgents':
|
||||
await this._sendAgentsList();
|
||||
break;
|
||||
case 'getChronicleProjects':
|
||||
await this._sendChronicleProjects();
|
||||
break;
|
||||
case 'createChronicleProject':
|
||||
await this._createChronicleProject();
|
||||
break;
|
||||
case 'setChronicleProject':
|
||||
await this._setActiveChronicleProject(data.id);
|
||||
break;
|
||||
case 'openChronicleFolder':
|
||||
await this._openChronicleFolder();
|
||||
break;
|
||||
case 'getChronicleRecords':
|
||||
await this._sendChronicleRecords();
|
||||
break;
|
||||
case 'openChronicleRecord':
|
||||
await this._openChronicleRecord(data.path);
|
||||
break;
|
||||
case 'writeChroniclePlanning':
|
||||
await this._writeChroniclePlanningFromCurrentChat();
|
||||
break;
|
||||
case 'writeChronicleDiscussion':
|
||||
await this._writeChronicleDiscussionFromCurrentChat();
|
||||
break;
|
||||
case 'writeChronicleDecision':
|
||||
await this._writeChronicleDecisionFromInput();
|
||||
break;
|
||||
case 'writeChronicleDevelopment':
|
||||
await this._writeChronicleDevelopmentFromCurrentChat();
|
||||
break;
|
||||
case 'writeChronicleBug':
|
||||
await this._writeChronicleBugFromInput();
|
||||
break;
|
||||
case 'writeChronicleRetrospective':
|
||||
await this._writeChronicleRetrospectiveFromInput();
|
||||
break;
|
||||
case 'writeChronicleRecord':
|
||||
await this._writeChronicleRecord(data.recordType);
|
||||
break;
|
||||
case 'createAgent':
|
||||
await this._createAgent();
|
||||
break;
|
||||
@@ -899,6 +943,568 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
});
|
||||
}
|
||||
|
||||
private _getChronicleProjects(): ProjectProfile[] {
|
||||
const raw = this._context.globalState.get<ProjectProfile[]>(SidebarChatProvider.chronicleProjectsStateKey, []) || [];
|
||||
const valid = raw.filter((profile: ProjectProfile) =>
|
||||
profile
|
||||
&& typeof profile.projectId === 'string'
|
||||
&& typeof profile.projectName === 'string'
|
||||
&& typeof profile.recordRoot === 'string'
|
||||
);
|
||||
|
||||
if (valid.length > 0) return valid;
|
||||
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (!workspaceRoot) return [];
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const projectName = path.basename(workspaceRoot) || 'Current Project';
|
||||
return [{
|
||||
projectId: this._slugify(projectName),
|
||||
projectName,
|
||||
projectRoot: workspaceRoot,
|
||||
recordRoot: path.join(workspaceRoot, 'docs', 'records', projectName),
|
||||
description: 'Auto-detected current workspace project.',
|
||||
corePurpose: 'Capture project planning, decisions, development notes, bugs, and retrospectives as Markdown.',
|
||||
targetUsers: ['Project developer'],
|
||||
avoidDirections: ['Do not tightly couple records to chat execution internals.'],
|
||||
detailLevel: 'standard',
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}];
|
||||
}
|
||||
|
||||
private async _putChronicleProjects(projects: ProjectProfile[]) {
|
||||
await this._context.globalState.update(SidebarChatProvider.chronicleProjectsStateKey, projects);
|
||||
}
|
||||
|
||||
private _getActiveChronicleProject(): ProjectProfile | null {
|
||||
const projects = this._getChronicleProjects();
|
||||
if (projects.length === 0) return null;
|
||||
const activeId = this._context.globalState.get<string>(SidebarChatProvider.activeChronicleProjectStateKey, '');
|
||||
return projects.find(project => project.projectId === activeId) || projects[0];
|
||||
}
|
||||
|
||||
private async _sendChronicleProjects() {
|
||||
if (!this._view) return;
|
||||
const projects = this._getChronicleProjects();
|
||||
const active = this._getActiveChronicleProject();
|
||||
this._view.webview.postMessage({
|
||||
type: 'chronicleProjects',
|
||||
value: {
|
||||
activeProjectId: active?.projectId || '',
|
||||
projects: projects.map(project => ({
|
||||
id: project.projectId,
|
||||
name: project.projectName,
|
||||
root: project.projectRoot || '',
|
||||
recordRoot: project.recordRoot,
|
||||
description: project.description || ''
|
||||
}))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async _createChronicleProject() {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
|
||||
const defaultName = workspaceRoot ? path.basename(workspaceRoot) : 'New Project';
|
||||
|
||||
const projectName = await vscode.window.showInputBox({
|
||||
prompt: 'Project name for Chronicle records',
|
||||
value: defaultName,
|
||||
validateInput: (value) => value.trim() ? null : 'Project name is required.'
|
||||
});
|
||||
if (!projectName) return;
|
||||
|
||||
const description = await vscode.window.showInputBox({
|
||||
prompt: 'One-line project description',
|
||||
value: 'Project planning, decisions, development logs, and bug records.'
|
||||
});
|
||||
if (description === undefined) return;
|
||||
|
||||
const projectRoot = await vscode.window.showInputBox({
|
||||
prompt: 'Project root path',
|
||||
value: workspaceRoot,
|
||||
validateInput: (value) => value.trim() ? null : 'Project root is required.'
|
||||
});
|
||||
if (!projectRoot) return;
|
||||
|
||||
const defaultRecordRoot = path.join(projectRoot.trim(), 'docs', 'records', projectName.trim());
|
||||
const recordRoot = await vscode.window.showInputBox({
|
||||
prompt: 'Markdown record folder path',
|
||||
value: defaultRecordRoot,
|
||||
validateInput: (value) => value.trim() ? null : 'Record folder path is required.'
|
||||
});
|
||||
if (!recordRoot) return;
|
||||
|
||||
const corePurpose = await vscode.window.showInputBox({
|
||||
prompt: 'Core project purpose or guardrail',
|
||||
value: 'Keep project knowledge traceable through Markdown records.'
|
||||
});
|
||||
if (corePurpose === undefined) return;
|
||||
|
||||
const detailChoice = await vscode.window.showQuickPick([
|
||||
{ label: 'standard', description: 'Request, question intent, decisions, planning, development, and bugs' },
|
||||
{ label: 'simple', description: 'Request summary, decisions, and implementation result' },
|
||||
{ label: 'detailed', description: 'Standard records plus alternatives, lessons, and retrospectives' }
|
||||
], {
|
||||
placeHolder: 'Chronicle record detail level'
|
||||
});
|
||||
if (!detailChoice) return;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const projects = this._getChronicleProjects();
|
||||
const idBase = this._slugify(projectName.trim());
|
||||
let projectId = idBase;
|
||||
let suffix = 2;
|
||||
while (projects.some(project => project.projectId === projectId)) {
|
||||
projectId = `${idBase}-${suffix++}`;
|
||||
}
|
||||
|
||||
const profile: ProjectProfile = {
|
||||
projectId,
|
||||
projectName: projectName.trim(),
|
||||
projectRoot: projectRoot.trim(),
|
||||
recordRoot: recordRoot.trim(),
|
||||
description: description.trim(),
|
||||
corePurpose: corePurpose.trim(),
|
||||
targetUsers: ['Project developer'],
|
||||
avoidDirections: ['Do not mix records across projects.', 'Do not tightly couple records to core agent execution.'],
|
||||
detailLevel: detailChoice.label as ProjectProfile['detailLevel'],
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
|
||||
this._chronicle.ensureProject(profile);
|
||||
const nextProjects = [...projects.filter(project => project.projectId !== profile.projectId), profile];
|
||||
await this._putChronicleProjects(nextProjects);
|
||||
await this._context.globalState.update(SidebarChatProvider.activeChronicleProjectStateKey, profile.projectId);
|
||||
await this._sendChronicleProjects();
|
||||
this.injectSystemMessage(`**[Designer Project Created]** ${profile.projectName}\n\`${profile.recordRoot}\``);
|
||||
}
|
||||
|
||||
private async _setActiveChronicleProject(projectId: string) {
|
||||
if (!projectId || projectId === 'new') {
|
||||
await this._createChronicleProject();
|
||||
return;
|
||||
}
|
||||
|
||||
const target = this._getChronicleProjects().find(project => project.projectId === projectId);
|
||||
if (!target) return;
|
||||
await this._context.globalState.update(SidebarChatProvider.activeChronicleProjectStateKey, target.projectId);
|
||||
await this._sendChronicleProjects();
|
||||
await this._sendChronicleRecords();
|
||||
this.injectSystemMessage(`**[Designer Project Selected]** ${target.projectName}\n\`${target.recordRoot}\``);
|
||||
}
|
||||
|
||||
private async _openChronicleFolder() {
|
||||
const profile = this._getActiveChronicleProject();
|
||||
if (!profile) {
|
||||
vscode.window.showWarningMessage('No Chronicle project is selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this._chronicle.ensureProject(profile);
|
||||
await vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(profile.recordRoot));
|
||||
} catch (err: any) {
|
||||
vscode.window.showErrorMessage(`Failed to open Chronicle folder: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async _sendChronicleRecords() {
|
||||
if (!this._view) return;
|
||||
const profile = this._getActiveChronicleProject();
|
||||
if (!profile) {
|
||||
this._view.webview.postMessage({ type: 'chronicleRecords', value: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const records = this._chronicle.listRecords(profile).map(record => ({
|
||||
section: record.section,
|
||||
fileName: record.fileName,
|
||||
path: record.filePath,
|
||||
relativePath: record.relativePath,
|
||||
updatedAt: record.updatedAt
|
||||
}));
|
||||
this._view.webview.postMessage({ type: 'chronicleRecords', value: records });
|
||||
} catch (err: any) {
|
||||
vscode.window.showErrorMessage(`Failed to list Chronicle records: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async _openChronicleRecord(recordPath: string) {
|
||||
const profile = this._getActiveChronicleProject();
|
||||
if (!profile || !recordPath) {
|
||||
vscode.window.showWarningMessage('Select a Chronicle record first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const root = path.resolve(profile.recordRoot);
|
||||
const target = path.resolve(recordPath);
|
||||
if (!target.startsWith(`${root}${path.sep}`) || path.extname(target) !== '.md') {
|
||||
vscode.window.showErrorMessage('Selected Chronicle record path is not valid.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(target)) {
|
||||
vscode.window.showErrorMessage('Selected Chronicle record no longer exists.');
|
||||
await this._sendChronicleRecords();
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(target);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
}
|
||||
|
||||
private async _writeChroniclePlanningFromCurrentChat() {
|
||||
const profile = this._getActiveChronicleProject();
|
||||
if (!profile) {
|
||||
vscode.window.showWarningMessage('Select or create a Designer project first.');
|
||||
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 featureName = await vscode.window.showInputBox({
|
||||
prompt: 'Feature name for the planning document',
|
||||
value: this._summarizeForTitle(latestUser || 'Project Chronicle Feature')
|
||||
});
|
||||
if (!featureName) return;
|
||||
|
||||
try {
|
||||
const createdAt = new Date().toISOString();
|
||||
const result = this._chronicle.writePlanning(profile, {
|
||||
featureName: featureName.trim(),
|
||||
purpose: 'Record the reason, scope, direction, and success criteria before implementation.',
|
||||
background: this._summarizeTextForWiki(latestAssistant || latestUser),
|
||||
userIntent: this._summarizeTextForWiki(latestUser),
|
||||
sourceRequest: latestUser || 'No user request captured in the current chat.',
|
||||
scope: [
|
||||
'Create a project-specific planning record.',
|
||||
'Capture user intent and implementation direction.',
|
||||
'Keep the record independent from chat execution internals.'
|
||||
],
|
||||
outOfScope: [
|
||||
'Full automatic transcript capture.',
|
||||
'External database integration.',
|
||||
'Git automation.'
|
||||
],
|
||||
developmentDirection: 'Use Project Chronicle as a low-dependency Markdown record layer.',
|
||||
dependencyStrategy: 'Use local filesystem writes through the independent projectChronicle module.',
|
||||
expectedValue: 'Future work can understand why this feature exists and what decisions shaped it.',
|
||||
successCriteria: [
|
||||
'The planning document is created under the selected project record folder.',
|
||||
'The document includes user intent, scope, out-of-scope items, and success criteria.'
|
||||
],
|
||||
developerInstruction: 'Use this document as the implementation guardrail for the next development step.',
|
||||
createdAt
|
||||
});
|
||||
this._chronicle.appendTimeline(profile, [`Planning record created: ${result.relativePath}`], createdAt);
|
||||
vscode.window.showInformationMessage(`Chronicle planning saved: ${result.relativePath}`);
|
||||
await this._sendChronicleRecords();
|
||||
this.injectSystemMessage(`**[Chronicle Planning Saved]** \`${result.filePath}\``);
|
||||
} catch (err: any) {
|
||||
vscode.window.showErrorMessage(`Failed to write planning record: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async _writeChronicleDiscussionFromCurrentChat() {
|
||||
const profile = this._getActiveChronicleProject();
|
||||
if (!profile) {
|
||||
vscode.window.showWarningMessage('Select or create a Designer project first.');
|
||||
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 title = await vscode.window.showInputBox({
|
||||
prompt: 'Discussion title',
|
||||
value: this._summarizeForTitle(latestUser || 'Project Discussion')
|
||||
});
|
||||
if (!title) return;
|
||||
|
||||
const question = await vscode.window.showInputBox({
|
||||
prompt: 'AI question to record (optional)',
|
||||
value: ''
|
||||
});
|
||||
if (question === undefined) return;
|
||||
|
||||
let questions: any[] = [];
|
||||
if (question.trim()) {
|
||||
const reason = await vscode.window.showInputBox({
|
||||
prompt: 'Why was this question asked?',
|
||||
value: 'To avoid writing records to the wrong project or making an unclear design decision.'
|
||||
});
|
||||
if (reason === undefined) return;
|
||||
|
||||
const impact = await vscode.window.showInputBox({
|
||||
prompt: 'How does this question affect the decision?',
|
||||
value: 'It determines the correct project context, scope, or implementation path.'
|
||||
});
|
||||
if (impact === undefined) return;
|
||||
|
||||
questions = [{
|
||||
question: question.trim(),
|
||||
reason: reason.trim(),
|
||||
expectedInformation: 'Information needed to clarify project context, scope, or decision direction.',
|
||||
impactOnDecision: impact.trim()
|
||||
}];
|
||||
}
|
||||
|
||||
try {
|
||||
const createdAt = new Date().toISOString();
|
||||
const result = this._chronicle.writeDiscussion(profile, {
|
||||
title: title.trim(),
|
||||
userRequest: this._summarizeTextForWiki(latestUser || 'No user request captured in the current chat.'),
|
||||
interpretedIntent: 'Capture the discussion as a reusable project knowledge record instead of leaving it only in chat history.',
|
||||
questions,
|
||||
discussions: [
|
||||
this._summarizeTextForWiki(latestAssistant || latestUser || 'No discussion detail captured yet.')
|
||||
],
|
||||
decisions: [],
|
||||
createdAt
|
||||
});
|
||||
this._chronicle.appendTimeline(profile, [`Discussion record created: ${result.relativePath}`], createdAt);
|
||||
vscode.window.showInformationMessage(`Chronicle discussion saved: ${result.relativePath}`);
|
||||
await this._sendChronicleRecords();
|
||||
this.injectSystemMessage(`**[Chronicle Discussion Saved]** \`${result.filePath}\``);
|
||||
} catch (err: any) {
|
||||
vscode.window.showErrorMessage(`Failed to write discussion record: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async _writeChronicleDecisionFromInput() {
|
||||
const profile = this._getActiveChronicleProject();
|
||||
if (!profile) {
|
||||
vscode.window.showWarningMessage('Select or create a Designer project first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const title = await vscode.window.showInputBox({
|
||||
prompt: 'Decision title',
|
||||
value: 'Use independent Markdown record module'
|
||||
});
|
||||
if (!title) return;
|
||||
|
||||
const decision = await vscode.window.showInputBox({
|
||||
prompt: 'Decision',
|
||||
value: 'Implement this behavior as an independent Project Chronicle module.'
|
||||
});
|
||||
if (decision === undefined) return;
|
||||
|
||||
const reason = await vscode.window.showInputBox({
|
||||
prompt: 'Decision reason',
|
||||
value: 'To reduce coupling and keep project records portable.'
|
||||
});
|
||||
if (reason === undefined) return;
|
||||
|
||||
const alternatives = await vscode.window.showInputBox({
|
||||
prompt: 'Rejected alternatives (comma-separated)',
|
||||
value: 'Integrate with Second Brain, integrate directly into Agent execution'
|
||||
});
|
||||
if (alternatives === undefined) return;
|
||||
|
||||
try {
|
||||
const createdAt = new Date().toISOString();
|
||||
const adrNumber = this._chronicle.nextAdrNumber(profile);
|
||||
const result = this._chronicle.writeDecision(profile, {
|
||||
title: title.trim(),
|
||||
status: 'accepted',
|
||||
context: 'A project record needs to capture not only what changed, but why the direction was chosen.',
|
||||
decision: decision.trim(),
|
||||
reason: reason.trim(),
|
||||
alternatives: alternatives.split(',').map(item => item.trim()).filter(Boolean),
|
||||
consequences: [
|
||||
'Records can evolve independently from chat and agent internals.',
|
||||
'Future automation can emit chronicle events without owning the core execution path.'
|
||||
],
|
||||
createdAt
|
||||
}, adrNumber);
|
||||
this._chronicle.appendTimeline(profile, [`Decision record created: ${result.relativePath}`], createdAt);
|
||||
vscode.window.showInformationMessage(`Chronicle decision saved: ${result.relativePath}`);
|
||||
await this._sendChronicleRecords();
|
||||
this.injectSystemMessage(`**[Chronicle Decision Saved]** \`${result.filePath}\``);
|
||||
} catch (err: any) {
|
||||
vscode.window.showErrorMessage(`Failed to write decision record: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async _writeChronicleDevelopmentFromCurrentChat() {
|
||||
const profile = this._getActiveChronicleProject();
|
||||
if (!profile) {
|
||||
vscode.window.showWarningMessage('Select or create a Designer project first.');
|
||||
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 featureName = await vscode.window.showInputBox({
|
||||
prompt: 'Feature name for the development log',
|
||||
value: this._summarizeForTitle(latestUser || 'Implementation Log')
|
||||
});
|
||||
if (!featureName) return;
|
||||
|
||||
try {
|
||||
const createdAt = new Date().toISOString();
|
||||
const result = this._chronicle.writeDevelopmentLog(profile, {
|
||||
featureName: featureName.trim(),
|
||||
purpose: 'Record the actual implementation outcome for later maintenance.',
|
||||
implementationSummary: this._summarizeTextForWiki(latestAssistant || 'Implementation summary was not captured from chat yet.'),
|
||||
architecture: 'Project Chronicle records are written through an independent Markdown module.',
|
||||
changedFiles: ['Capture exact changed files after verification.'],
|
||||
dependencyNotes: 'Keep dependencies limited to TypeScript, Node fs/path, and VS Code extension APIs.',
|
||||
bugs: [],
|
||||
lessons: [
|
||||
'Write implementation notes as soon as a stable development step finishes.',
|
||||
'Keep generated records project-specific.'
|
||||
],
|
||||
createdAt
|
||||
});
|
||||
this._chronicle.appendTimeline(profile, [`Development log created: ${result.relativePath}`], createdAt);
|
||||
vscode.window.showInformationMessage(`Chronicle development log saved: ${result.relativePath}`);
|
||||
await this._sendChronicleRecords();
|
||||
this.injectSystemMessage(`**[Chronicle Development Saved]** \`${result.filePath}\``);
|
||||
} catch (err: any) {
|
||||
vscode.window.showErrorMessage(`Failed to write development record: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async _writeChronicleBugFromInput() {
|
||||
const profile = this._getActiveChronicleProject();
|
||||
if (!profile) {
|
||||
vscode.window.showWarningMessage('Select or create a Designer project first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const title = await vscode.window.showInputBox({
|
||||
prompt: 'Bug title',
|
||||
value: 'record-generation-issue'
|
||||
});
|
||||
if (!title) return;
|
||||
|
||||
const symptom = await vscode.window.showInputBox({
|
||||
prompt: 'Bug symptom',
|
||||
value: 'Describe what failed or looked wrong.'
|
||||
});
|
||||
if (symptom === undefined) return;
|
||||
|
||||
const cause = await vscode.window.showInputBox({
|
||||
prompt: 'Bug cause',
|
||||
value: 'Cause is not confirmed yet.'
|
||||
});
|
||||
if (cause === undefined) return;
|
||||
|
||||
const fix = await vscode.window.showInputBox({
|
||||
prompt: 'Fix',
|
||||
value: 'Describe the fix or mitigation.'
|
||||
});
|
||||
if (fix === undefined) return;
|
||||
|
||||
try {
|
||||
const createdAt = new Date().toISOString();
|
||||
const bugNumber = this._chronicle.nextBugNumber(profile);
|
||||
const result = this._chronicle.writeBug(profile, {
|
||||
title: title.trim(),
|
||||
symptom: symptom.trim(),
|
||||
cause: cause.trim(),
|
||||
fix: fix.trim(),
|
||||
prevention: 'Validate project selection, record path, and write permissions before generating files.',
|
||||
createdAt
|
||||
}, bugNumber);
|
||||
this._chronicle.appendTimeline(profile, [`Bug record created: ${result.relativePath}`], createdAt);
|
||||
vscode.window.showInformationMessage(`Chronicle bug record saved: ${result.relativePath}`);
|
||||
await this._sendChronicleRecords();
|
||||
this.injectSystemMessage(`**[Chronicle Bug Saved]** \`${result.filePath}\``);
|
||||
} catch (err: any) {
|
||||
vscode.window.showErrorMessage(`Failed to write bug record: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async _writeChronicleRetrospectiveFromInput() {
|
||||
const profile = this._getActiveChronicleProject();
|
||||
if (!profile) {
|
||||
vscode.window.showWarningMessage('Select or create a Designer project first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const title = await vscode.window.showInputBox({
|
||||
prompt: 'Retrospective title',
|
||||
value: 'Project Chronicle Guard iteration'
|
||||
});
|
||||
if (!title) return;
|
||||
|
||||
const summary = await vscode.window.showInputBox({
|
||||
prompt: 'Work summary',
|
||||
value: 'Completed an incremental development step and recorded the outcome.'
|
||||
});
|
||||
if (summary === undefined) return;
|
||||
|
||||
const wentWell = await vscode.window.showInputBox({
|
||||
prompt: 'What went well? (comma-separated)',
|
||||
value: 'Kept the feature independent, Generated Markdown records, Preserved project context'
|
||||
});
|
||||
if (wentWell === undefined) return;
|
||||
|
||||
const toImprove = await vscode.window.showInputBox({
|
||||
prompt: 'What should improve? (comma-separated)',
|
||||
value: 'More automatic question intent capture, Richer record editing UI'
|
||||
});
|
||||
if (toImprove === undefined) return;
|
||||
|
||||
const nextActions = await vscode.window.showInputBox({
|
||||
prompt: 'Next actions (comma-separated)',
|
||||
value: 'Add tests, Improve Designer UI, Add event-based record capture'
|
||||
});
|
||||
if (nextActions === undefined) return;
|
||||
|
||||
try {
|
||||
const createdAt = new Date().toISOString();
|
||||
const result = this._chronicle.writeRetrospective(profile, {
|
||||
title: title.trim(),
|
||||
summary: summary.trim(),
|
||||
wentWell: wentWell.split(',').map(item => item.trim()).filter(Boolean),
|
||||
toImprove: toImprove.split(',').map(item => item.trim()).filter(Boolean),
|
||||
nextActions: nextActions.split(',').map(item => item.trim()).filter(Boolean),
|
||||
createdAt
|
||||
});
|
||||
this._chronicle.appendTimeline(profile, [`Retrospective created: ${result.relativePath}`], createdAt);
|
||||
vscode.window.showInformationMessage(`Chronicle retrospective saved: ${result.relativePath}`);
|
||||
await this._sendChronicleRecords();
|
||||
this.injectSystemMessage(`**[Chronicle Retrospective Saved]** \`${result.filePath}\``);
|
||||
} catch (err: any) {
|
||||
vscode.window.showErrorMessage(`Failed to write retrospective record: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async _writeChronicleRecord(recordType: string) {
|
||||
switch (recordType) {
|
||||
case 'planning':
|
||||
await this._writeChroniclePlanningFromCurrentChat();
|
||||
break;
|
||||
case 'discussion':
|
||||
await this._writeChronicleDiscussionFromCurrentChat();
|
||||
break;
|
||||
case 'decision':
|
||||
await this._writeChronicleDecisionFromInput();
|
||||
break;
|
||||
case 'development':
|
||||
await this._writeChronicleDevelopmentFromCurrentChat();
|
||||
break;
|
||||
case 'bug':
|
||||
await this._writeChronicleBugFromInput();
|
||||
break;
|
||||
case 'retrospective':
|
||||
await this._writeChronicleRetrospectiveFromInput();
|
||||
break;
|
||||
default:
|
||||
vscode.window.showWarningMessage('Select a Chronicle record type first.');
|
||||
}
|
||||
}
|
||||
|
||||
private _getAgentsDir(): string {
|
||||
const defaultPath = 'E:\\Wiki\\Agent\\.agent\\skills';
|
||||
if (fs.existsSync(defaultPath)) return defaultPath;
|
||||
@@ -1043,7 +1649,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
private async _handlePrompt(data: any) {
|
||||
if (!this._view) return;
|
||||
|
||||
const { value, model, internet, files, agentFile, negativePrompt } = data;
|
||||
const { value, model, internet, files, agentFile, negativePrompt, designerGuard, secondBrainTrace, secondBrainTraceDebug } = data;
|
||||
this._currentNegativePrompt = negativePrompt || '';
|
||||
this._currentSessionBrainId = getActiveBrainProfile().id;
|
||||
|
||||
@@ -1052,12 +1658,17 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
agentSkillContext = fs.readFileSync(agentFile, 'utf8');
|
||||
}
|
||||
|
||||
const designerContext = designerGuard !== false ? this._buildDesignerGuardContext() : undefined;
|
||||
|
||||
try {
|
||||
await this._agent.handlePrompt(value, model, {
|
||||
internetEnabled: internet,
|
||||
visionContent: files,
|
||||
agentSkillContext,
|
||||
negativePrompt
|
||||
negativePrompt,
|
||||
designerContext,
|
||||
secondBrainTraceEnabled: secondBrainTrace !== false,
|
||||
secondBrainTraceDebug: !!secondBrainTraceDebug
|
||||
});
|
||||
} catch (error: any) {
|
||||
logError('Prompt handling failed in sidebar provider.', { error: error?.message || String(error), promptPreview: summarizeText(value || '', 200) });
|
||||
@@ -1065,6 +1676,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
}
|
||||
}
|
||||
|
||||
private _buildDesignerGuardContext(): string {
|
||||
return buildProjectChronicleGuardContext(this._getActiveChronicleProject());
|
||||
}
|
||||
|
||||
private async _sendModels() {
|
||||
if (!this._view) return;
|
||||
try {
|
||||
@@ -1755,6 +2370,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
<div class="header-actions">
|
||||
<button class="icon-btn" id="newChatBtn" data-tooltip="New Chat">New</button>
|
||||
<button class="icon-btn" id="saveWikiRawBtn" data-tooltip="Save Wiki Raw">Wiki</button>
|
||||
<button class="icon-btn active" id="designerGuardBtn" data-tooltip="Chronicle Guard Mode: Auto">Guard</button>
|
||||
<button class="icon-btn active" id="brainTraceBtn" data-tooltip="Second Brain Trace Mode">Trace</button>
|
||||
<button class="icon-btn" id="brainTraceDebugBtn" data-tooltip="Second Brain Debug JSON">Dbg</button>
|
||||
<button class="icon-btn" id="multiAgentBtn" data-tooltip="Multi-Agent Mode">MA</button>
|
||||
<button class="icon-btn" id="internetBtn" data-tooltip="Internet Access">Web</button>
|
||||
<button class="icon-btn" id="historyBtn" data-tooltip="View History">Log</button>
|
||||
@@ -1786,6 +2404,35 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<div class="select-wrap"><select id="designerSel" title="Select Designer Project"></select></div>
|
||||
<div class="tool-group" aria-label="Designer actions">
|
||||
<button class="icon-btn" id="addDesignerBtn" data-tooltip="Create Designer Project">Add</button>
|
||||
<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>
|
||||
<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>
|
||||
<button class="icon-btn" id="openChronicleRecordBtn" data-tooltip="Open Selected Record">Open</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1864,7 +2511,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
}
|
||||
|
||||
function saveWebviewState(history) {
|
||||
vscode.setState({ history });
|
||||
const current = vscode.getState() || {};
|
||||
vscode.setState({ ...current, history });
|
||||
}
|
||||
|
||||
function saveUiState() {
|
||||
const current = vscode.getState() || {};
|
||||
vscode.setState({ ...current, designerGuardEnabled, secondBrainTraceEnabled, secondBrainTraceDebug });
|
||||
}
|
||||
|
||||
function renderHistory(history) {
|
||||
@@ -1989,6 +2642,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
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');
|
||||
const deleteAgentBtn = document.getElementById('deleteAgentBtn');
|
||||
@@ -2003,8 +2659,29 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
|
||||
let streamBody = null;
|
||||
let internetEnabled = false;
|
||||
let designerGuardEnabled = true;
|
||||
let secondBrainTraceEnabled = true;
|
||||
let secondBrainTraceDebug = false;
|
||||
let pendingFiles = [];
|
||||
let editMode = false;
|
||||
if (previousState && typeof previousState.designerGuardEnabled === 'boolean') {
|
||||
designerGuardEnabled = previousState.designerGuardEnabled;
|
||||
}
|
||||
if (previousState && typeof previousState.secondBrainTraceEnabled === 'boolean') {
|
||||
secondBrainTraceEnabled = previousState.secondBrainTraceEnabled;
|
||||
}
|
||||
if (previousState && typeof previousState.secondBrainTraceDebug === 'boolean') {
|
||||
secondBrainTraceDebug = previousState.secondBrainTraceDebug;
|
||||
}
|
||||
const initialGuardBtn = document.getElementById('designerGuardBtn');
|
||||
initialGuardBtn.classList.toggle('active', designerGuardEnabled);
|
||||
initialGuardBtn.setAttribute('data-tooltip', designerGuardEnabled ? 'Chronicle Guard Mode: On' : 'Chronicle Guard Mode: Off');
|
||||
const initialTraceBtn = document.getElementById('brainTraceBtn');
|
||||
initialTraceBtn.classList.toggle('active', secondBrainTraceEnabled);
|
||||
initialTraceBtn.setAttribute('data-tooltip', secondBrainTraceEnabled ? 'Second Brain Trace Mode: On' : 'Second Brain Trace Mode: Off');
|
||||
const initialTraceDebugBtn = document.getElementById('brainTraceDebugBtn');
|
||||
initialTraceDebugBtn.classList.toggle('active', secondBrainTraceDebug);
|
||||
initialTraceDebugBtn.setAttribute('data-tooltip', secondBrainTraceDebug ? 'Second Brain Debug JSON: On' : 'Second Brain Debug JSON: Off');
|
||||
|
||||
function fmt(text) { return marked.parse(text || ''); }
|
||||
|
||||
@@ -2211,6 +2888,39 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
vscode.postMessage({ type: 'getAgentContent', path: msg.selected });
|
||||
}
|
||||
break;
|
||||
case 'chronicleProjects':
|
||||
designerSel.innerHTML = '';
|
||||
msg.value.projects.forEach(p => {
|
||||
const o = document.createElement('option');
|
||||
o.value = p.id;
|
||||
o.innerText = p.name;
|
||||
o.title = p.recordRoot;
|
||||
if (p.id === msg.value.activeProjectId) o.selected = true;
|
||||
designerSel.appendChild(o);
|
||||
});
|
||||
const newDesignerOpt = document.createElement('option');
|
||||
newDesignerOpt.value = 'new';
|
||||
newDesignerOpt.innerText = '+ Add Designer Project...';
|
||||
designerSel.appendChild(newDesignerOpt);
|
||||
vscode.postMessage({ type: 'getChronicleRecords' });
|
||||
break;
|
||||
case 'chronicleRecords':
|
||||
chronicleRecordSel.innerHTML = '';
|
||||
if (!msg.value || msg.value.length === 0) {
|
||||
const emptyRecordOpt = document.createElement('option');
|
||||
emptyRecordOpt.value = '';
|
||||
emptyRecordOpt.innerText = 'No records yet';
|
||||
chronicleRecordSel.appendChild(emptyRecordOpt);
|
||||
break;
|
||||
}
|
||||
msg.value.forEach(record => {
|
||||
const o = document.createElement('option');
|
||||
o.value = record.path;
|
||||
o.innerText = record.relativePath;
|
||||
o.title = record.path;
|
||||
chronicleRecordSel.appendChild(o);
|
||||
});
|
||||
break;
|
||||
case 'agentContent':
|
||||
agentPrompt.value = msg.value;
|
||||
negativePrompt.value = msg.negativePrompt || '';
|
||||
@@ -2324,7 +3034,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
internet: internetEnabled,
|
||||
files: pendingFiles.length > 0 ? pendingFiles : undefined,
|
||||
agentFile: agentSel.value === 'none' ? undefined : agentSel.value,
|
||||
negativePrompt: negativePrompt.value.trim() || undefined
|
||||
negativePrompt: negativePrompt.value.trim() || undefined,
|
||||
designerGuard: designerGuardEnabled,
|
||||
secondBrainTrace: secondBrainTraceEnabled,
|
||||
secondBrainTraceDebug
|
||||
});
|
||||
input.value = ''; input.style.height = 'auto'; pendingFiles = []; renderAttachments();
|
||||
// 전송 완료 후 Draft State 리셋 + Stop 버튼 표시
|
||||
@@ -2370,6 +3083,27 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
document.getElementById('internetBtn').onclick = () => {
|
||||
internetEnabled = !internetEnabled; document.getElementById('internetBtn').classList.toggle('active', internetEnabled);
|
||||
};
|
||||
document.getElementById('designerGuardBtn').onclick = () => {
|
||||
designerGuardEnabled = !designerGuardEnabled;
|
||||
const btn = document.getElementById('designerGuardBtn');
|
||||
btn.classList.toggle('active', designerGuardEnabled);
|
||||
btn.setAttribute('data-tooltip', designerGuardEnabled ? 'Chronicle Guard Mode: On' : 'Chronicle Guard Mode: Off');
|
||||
saveUiState();
|
||||
};
|
||||
document.getElementById('brainTraceBtn').onclick = () => {
|
||||
secondBrainTraceEnabled = !secondBrainTraceEnabled;
|
||||
const btn = document.getElementById('brainTraceBtn');
|
||||
btn.classList.toggle('active', secondBrainTraceEnabled);
|
||||
btn.setAttribute('data-tooltip', secondBrainTraceEnabled ? 'Second Brain Trace Mode: On' : 'Second Brain Trace Mode: Off');
|
||||
saveUiState();
|
||||
};
|
||||
document.getElementById('brainTraceDebugBtn').onclick = () => {
|
||||
secondBrainTraceDebug = !secondBrainTraceDebug;
|
||||
const btn = document.getElementById('brainTraceDebugBtn');
|
||||
btn.classList.toggle('active', secondBrainTraceDebug);
|
||||
btn.setAttribute('data-tooltip', secondBrainTraceDebug ? 'Second Brain Debug JSON: On' : 'Second Brain Debug JSON: Off');
|
||||
saveUiState();
|
||||
};
|
||||
|
||||
let multiAgentEnabled = false;
|
||||
const setMultiAgentUi = (enabled) => {
|
||||
@@ -2427,6 +3161,15 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
}
|
||||
};
|
||||
|
||||
designerSel.onchange = () => {
|
||||
if (designerSel.value === 'new') {
|
||||
vscode.postMessage({ type: 'createChronicleProject' });
|
||||
} else {
|
||||
vscode.postMessage({ type: 'setChronicleProject', id: designerSel.value });
|
||||
vscode.postMessage({ type: 'getChronicleRecords' });
|
||||
}
|
||||
};
|
||||
|
||||
// Handle initial state and state updates from extension
|
||||
window.addEventListener('message', e => {
|
||||
const msg = e.data;
|
||||
@@ -2473,8 +3216,22 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
vscode.postMessage({ type: 'deleteAgent', path: agentSel.value });
|
||||
};
|
||||
|
||||
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;
|
||||
vscode.postMessage({ type: 'openChronicleRecord', path: chronicleRecordSel.value });
|
||||
};
|
||||
|
||||
vscode.postMessage({ type: 'getModels' });
|
||||
vscode.postMessage({ type: 'getAgents' });
|
||||
vscode.postMessage({ type: 'getChronicleProjects' });
|
||||
vscode.postMessage({ type: 'getChronicleRecords' });
|
||||
vscode.postMessage({ type: 'ready' });
|
||||
|
||||
// --- Proactive Behavioral Tracking ---
|
||||
|
||||
@@ -150,6 +150,8 @@ Core behavior:
|
||||
- Do not output hidden reasoning labels such as [PROBLEM], [GOAL], [REASONING], Phase 0, Fidelity Lock-in, or process manifestos.
|
||||
- For substantial answers, write readable Markdown using ## and ### headings, short paragraphs, bullets, and tables where useful.
|
||||
- Avoid wall-of-text output. Make the answer scannable before adding detail.
|
||||
- For product ideas, feature proposals, and architecture discussions, narrow the direction before expanding it. Prefer a practical MVP first, then separate later expansion ideas.
|
||||
- Avoid inflated consulting language. Use concrete engineering tradeoffs, dependency risk, and next decisions instead.
|
||||
|
||||
Available action tags:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user