release: v2.0.1 - Advanced Knowledge Mix & Architectural Intelligence

This commit is contained in:
g1nation
2026-05-13 22:05:39 +09:00
parent c32b17377b
commit e85e11aac6
23 changed files with 1758 additions and 78 deletions
+323 -6
View File
@@ -11,7 +11,8 @@ import {
logError,
logInfo,
resolveEngine,
summarizeText
summarizeText,
openInEditorGroup
} from './utils';
import { getConfig } from './config';
import { AgentExecutor, ChatMessage } from './agent';
@@ -26,6 +27,13 @@ import { handleAgentMessage } from './sidebar/agentHandlers';
import { getOrCreateAgentEntry, resolveScopeForAgent } from './skills/agentKnowledgeMap';
import { estimateModelParamsB } from './lib/contextManager';
import { loadExternalSkills, formatSkillsAsPromptBlock } from './skills/externalSkillLoader';
import {
buildOrRefreshArchitectureDoc,
architectureDocPathFor,
formatArchitectureContextForPrompt,
scanProject,
} from './features/projectArchitecture';
import { detectProjectIntent, KnownProject } from './features/projectArchitecture/intentDetector';
export interface SidebarLmStudioDeps {
lifecycle: ModelLifecycleManager;
@@ -73,6 +81,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
_modelsCache: { url: string; models: string[]; online: boolean; fetchedAt: number } | null = null;
static readonly MODELS_CACHE_TTL_MS = 30000;
/** Active file-system watcher for the project-architecture auto-update. Disposed on switch/detach. */
private _archWatcher?: vscode.FileSystemWatcher;
/** Debounce timer for the architecture watcher. */
private _archWatchDebounce?: NodeJS.Timeout;
/** Project ID the current watcher is watching — kept so we don't double-register. */
private _archWatchedProjectId?: string;
constructor(
readonly _extensionUri: vscode.Uri,
readonly _context: vscode.ExtensionContext,
@@ -957,6 +972,277 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
await this._context.globalState.update(SidebarChatProvider.chronicleProjectsStateKey, projects);
}
// ─── Project Architecture Context (Feature 2) ──────────────────────────────
//
// Activation flow:
// 1. Chat preprocessor (or an explicit "Activate" button) calls
// _tryActivateArchitectureFromText(latestUserMessage).
// 2. If the text yields a known/inferable project, we set it active,
// ensure the architecture doc exists, register the file watcher,
// and broadcast the state to the webview as a chip.
// 3. On every subsequent prompt, _handlePrompt reads
// _buildProjectArchitectureContext() and injects it into the model
// call. Detach → empty context + watcher disposed.
/** True if the active project has its architecture doc auto-attached. */
_isArchitectureAutoAttached(): boolean {
const p = this._getActiveChronicleProject();
return !!(p && p.architectureDocPath && p.architectureAutoAttach !== false);
}
/**
* Try to resolve a project handle from arbitrary user text. Combines:
* • Korean / English natural-language activation phrasing.
* • Absolute filesystem paths.
* • The existing Chronicle project list as ground truth for name matches.
*/
_detectProjectFromText(text: string): KnownProject | null {
const known = this._getChronicleProjects().map<KnownProject>((p) => ({
projectId: p.projectId,
projectName: p.projectName,
projectRoot: p.projectRoot,
}));
const hit = detectProjectIntent(text || '', known);
return hit?.project ?? null;
}
/**
* Activate (or refresh) architecture context for the project resolved from
* `text`. No-op when no project is detected. Returns the activated profile
* id, or `null` if nothing changed. Side-effects: writes the architecture
* doc, marks the project active, broadcasts the chip state.
*/
async _tryActivateArchitectureFromText(text: string): Promise<string | null> {
const detected = this._detectProjectFromText(text);
if (!detected) return null;
return this._activateArchitectureForProject(detected.projectId, {
fallbackName: detected.projectName,
fallbackRoot: detected.projectRoot,
});
}
/**
* Make `projectId` the active project, ensure its architecture doc exists,
* and register the file watcher. If the project isn't in the chronicle
* store yet (path-only match), materialise a minimal profile so subsequent
* turns can find it.
*/
async _activateArchitectureForProject(
projectId: string,
opts: { fallbackName?: string; fallbackRoot?: string } = {}
): Promise<string | null> {
const projects = this._getChronicleProjects();
let profile = projects.find((p) => p.projectId === projectId);
// Materialise a stub when the user references a project by path that
// isn't yet registered. We use the path's basename as the name and the
// standard records location as recordRoot so existing Chronicle code
// keeps working.
if (!profile) {
const root = opts.fallbackRoot || '';
if (!root) {
logError('architecture: cannot activate without project root.', { projectId });
return null;
}
const name = opts.fallbackName || path.basename(root) || projectId;
const now = new Date().toISOString();
profile = {
projectId,
projectName: name,
projectRoot: root,
recordRoot: path.join(root, 'docs', 'records', name),
description: 'Auto-created by Project Architecture activation.',
corePurpose: '',
detailLevel: 'standard',
createdAt: now,
updatedAt: now,
};
projects.push(profile);
await this._putChronicleProjects(projects);
}
if (!profile.projectRoot) {
logError('architecture: profile has no projectRoot; cannot scan.', { projectId });
return null;
}
// Generate or refresh the doc. Always idempotent — the generator
// preserves user-owned sections.
const result = buildOrRefreshArchitectureDoc(profile.projectRoot, profile.projectName);
const now = new Date().toISOString();
const updated: ProjectProfile = {
...profile,
architectureDocPath: result.docPath,
architectureAutoAttach: profile.architectureAutoAttach ?? true,
architectureAutoUpdate: profile.architectureAutoUpdate ?? true,
architectureLastUpdated: now,
architectureLastScanSignature: result.scan.signature,
updatedAt: now,
};
const next = projects.map((p) => (p.projectId === updated.projectId ? updated : p));
await this._putChronicleProjects(next);
await this._context.globalState.update(SidebarChatProvider.activeChronicleProjectStateKey, projectId);
// (Re)register the watcher for this project.
this._registerArchitectureWatcher(updated);
// Tell the webview to show / refresh the chip.
await this._sendArchitectureStatus();
logInfo('architecture: activated.', {
projectId, docPath: result.docPath, created: result.created,
});
return projectId;
}
/** Detach project mode: stop auto-attaching the doc and dispose the watcher. */
async _detachArchitecture(): Promise<void> {
const profile = this._getActiveChronicleProject();
if (!profile) {
this._disposeArchitectureWatcher();
await this._sendArchitectureStatus();
return;
}
const projects = this._getChronicleProjects();
const next = projects.map((p) => p.projectId === profile.projectId
? { ...p, architectureAutoAttach: false }
: p);
await this._putChronicleProjects(next);
this._disposeArchitectureWatcher();
await this._sendArchitectureStatus();
logInfo('architecture: detached.', { projectId: profile.projectId });
}
/** Force a refresh of the architecture doc for the active project. */
async _refreshArchitecture(): Promise<void> {
const profile = this._getActiveChronicleProject();
if (!profile || !profile.projectRoot) {
this._view?.webview.postMessage({
type: 'architectureRefreshFailed',
value: { reason: 'no-active-project' },
});
return;
}
const result = buildOrRefreshArchitectureDoc(profile.projectRoot, profile.projectName);
const now = new Date().toISOString();
const projects = this._getChronicleProjects();
const next = projects.map((p) => p.projectId === profile.projectId
? {
...p,
architectureDocPath: result.docPath,
architectureLastUpdated: now,
architectureLastScanSignature: result.scan.signature,
updatedAt: now,
}
: p);
await this._putChronicleProjects(next);
await this._sendArchitectureStatus();
}
/**
* Build the `projectArchitectureContext` string for the active prompt.
* Returns empty string when auto-attach is off or the doc is missing —
* agent.ts then treats it as "no block" and emits nothing extra.
*/
_buildProjectArchitectureContext(): string {
const p = this._getActiveChronicleProject();
if (!p || p.architectureAutoAttach === false || !p.architectureDocPath) return '';
if (!fs.existsSync(p.architectureDocPath)) return '';
return formatArchitectureContextForPrompt({
projectName: p.projectName,
docPath: p.architectureDocPath,
lastUpdated: p.architectureLastUpdated,
});
}
/** Webview chip data — shown above the input box when active. */
async _sendArchitectureStatus(): Promise<void> {
if (!this._view) return;
const p = this._getActiveChronicleProject();
const active = !!(p && p.architectureDocPath && p.architectureAutoAttach !== false);
this._view.webview.postMessage({
type: 'architectureStatus',
value: active && p
? {
active: true,
projectId: p.projectId,
projectName: p.projectName,
docPath: p.architectureDocPath,
lastUpdated: p.architectureLastUpdated || '',
autoUpdate: p.architectureAutoUpdate !== false,
}
: { active: false },
});
}
/** Open the architecture doc in editor group 2. */
async _openArchitectureDoc(): Promise<void> {
const p = this._getActiveChronicleProject();
if (!p || !p.architectureDocPath) return;
try {
const doc = await vscode.workspace.openTextDocument(p.architectureDocPath);
await vscode.window.showTextDocument(doc, {
viewColumn: vscode.ViewColumn.Two,
preview: false,
});
} catch (e: any) {
vscode.window.showErrorMessage(`Architecture doc 열기 실패: ${e?.message ?? e}`);
}
}
/**
* Register a debounced watcher over the project root. Only structural
* changes regen the doc — the signature hash decides whether to write.
* Files inside node_modules / out / dist are filtered by the glob to keep
* the noise floor sane during normal development.
*/
private _registerArchitectureWatcher(profile: ProjectProfile): void {
if (!profile.projectRoot) return;
if (this._archWatchedProjectId === profile.projectId && this._archWatcher) return;
this._disposeArchitectureWatcher();
if (profile.architectureAutoUpdate === false) {
this._archWatchedProjectId = profile.projectId;
return;
}
const pattern = new vscode.RelativePattern(
profile.projectRoot,
'{package.json,tsconfig.json,README.md,src/**,media/**,docs/**,tests/**,core_py/**,lib/**,app/**}'
);
const watcher = vscode.workspace.createFileSystemWatcher(pattern);
const onChange = () => this._scheduleArchitectureRefresh();
watcher.onDidCreate(onChange);
watcher.onDidDelete(onChange);
watcher.onDidChange(onChange);
this._archWatcher = watcher;
this._archWatchedProjectId = profile.projectId;
logInfo('architecture: watcher registered.', { projectId: profile.projectId, root: profile.projectRoot });
}
private _disposeArchitectureWatcher(): void {
try { this._archWatcher?.dispose(); } catch { /* noop */ }
this._archWatcher = undefined;
if (this._archWatchDebounce) { clearTimeout(this._archWatchDebounce); this._archWatchDebounce = undefined; }
this._archWatchedProjectId = undefined;
}
private _scheduleArchitectureRefresh(): void {
if (this._archWatchDebounce) clearTimeout(this._archWatchDebounce);
// 6 s debounce: long enough that a "save file" burst settles into one
// regen, short enough that the chip's "updated 2m ago" badge stays
// believable.
this._archWatchDebounce = setTimeout(async () => {
const profile = this._getActiveChronicleProject();
if (!profile || !profile.projectRoot || profile.architectureAutoUpdate === false) return;
try {
// Cheap signature check first — most file events don't change shape.
const scan = scanProject(profile.projectRoot, profile.projectName);
if (scan.signature === profile.architectureLastScanSignature) return;
await this._refreshArchitecture();
} catch (e: any) {
logError('architecture: auto-refresh failed.', { error: e?.message ?? String(e) });
}
}, 6000);
}
_getActiveChronicleProject(): ProjectProfile | null {
const projects = this._getChronicleProjects();
if (projects.length === 0) return null;
@@ -1132,8 +1418,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
return;
}
const doc = await vscode.workspace.openTextDocument(target);
await vscode.window.showTextDocument(doc);
await openInEditorGroup(target);
}
async _writeChroniclePlanningFromCurrentChat() {
@@ -1743,8 +2028,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
fs.writeFileSync(filePath, `# Agent Persona: ${safeName}\n\nAdd your instructions here...\n`, 'utf8');
}
const doc = await vscode.workspace.openTextDocument(filePath);
await vscode.window.showTextDocument(doc);
await openInEditorGroup(filePath);
await this._sendAgentsList();
}
@@ -1820,6 +2104,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
this._currentSessionBrainId = selectedBrainId;
let agentSkillContext = undefined;
// Per-agent model override: if the active agent has a pinned model in the
// knowledge map, it wins over the model the webview just sent. Falls back
// to the incoming `model` (which is the global default the user picked).
let effectiveModel: string = typeof model === 'string' ? model : '';
if (agentFile && agentFile !== 'none' && fs.existsSync(agentFile)) {
const fileContent = fs.readFileSync(agentFile, 'utf8');
// Guard: a freshly-created agent still has only the placeholder template
@@ -1842,6 +2130,21 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
if (block) {
agentSkillContext = `${agentSkillContext.trim()}\n\n${block}`;
}
// Apply the per-agent model override, if any.
const pinned = entry.model?.trim();
if (pinned && pinned !== effectiveModel) {
logInfo('Per-agent model override applied.', {
agent: entry.name,
requested: effectiveModel,
pinned,
});
effectiveModel = pinned;
// Inform the webview so its UI can reflect the model that's actually in use.
this._view?.webview.postMessage({
type: 'agentModelOverride',
value: { agent: entry.name, model: pinned },
});
}
} catch (e: any) {
logError('External skill load failed.', { error: e?.message || String(e) });
}
@@ -1850,6 +2153,19 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
const designerContext = designerGuard !== false ? this._buildDesignerGuardContext() : undefined;
// Project Architecture activation (Feature 2): if the user just said
// "나 X 프로젝트 진행할 거야" / mentioned an absolute path / etc., switch
// to that project's mode before assembling the prompt. Best-effort:
// failures here never block the actual answer.
if (typeof value === 'string' && value.trim().length > 0) {
try {
await this._tryActivateArchitectureFromText(value);
} catch (e: any) {
logError('architecture: intent detection failed.', { error: e?.message ?? String(e) });
}
}
const projectArchitectureContext = this._buildProjectArchitectureContext();
// [File Processing v2] 파일 타입별 분류 처리
let processedPrompt = value || '';
let imageFiles: any[] | undefined = undefined;
@@ -1940,13 +2256,14 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
}
try {
await this._agent.handlePrompt(processedPrompt, model, {
await this._agent.handlePrompt(processedPrompt, effectiveModel || model, {
internetEnabled: internet,
visionContent: imageFiles,
agentSkillContext,
agentSkillFile: typeof agentFile === 'string' ? agentFile : undefined,
negativePrompt,
designerContext,
projectArchitectureContext: projectArchitectureContext || undefined,
secondBrainTraceEnabled: secondBrainTrace !== false,
secondBrainTraceDebug: !!secondBrainTraceDebug,
brainProfileId: selectedBrainId