release: v2.0.1 - Advanced Knowledge Mix & Architectural Intelligence
This commit is contained in:
+323
-6
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user