release: v2.0.6 - Intelligence & UX Optimization (2026-05-14)

This commit is contained in:
g1nation
2026-05-14 00:13:54 +09:00
parent 39386f90b5
commit f1d5dbf031
18 changed files with 592 additions and 59 deletions
+184 -11
View File
@@ -1121,8 +1121,16 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
logInfo('architecture: detached.', { projectId: profile.projectId });
}
/** Force a refresh of the architecture doc for the active project. */
/**
* Force a refresh of the architecture doc for the active project.
*
* Always rewrites the auto-managed block (so the "Last Refresh" stamp +
* stats reflect the click). Emits an `architectureRefreshResult` event
* with the per-file work breakdown — that's what makes the operation
* visibly trustworthy in the UI (no more "0.1s, nothing visible").
*/
async _refreshArchitecture(): Promise<void> {
const startedAt = Date.now();
const profile = this._getActiveChronicleProject();
if (!profile || !profile.projectRoot) {
this._view?.webview.postMessage({
@@ -1145,6 +1153,111 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
: p);
await this._putChronicleProjects(next);
await this._sendArchitectureStatus();
// Tell the webview exactly what the scan did so the user can
// trust the "Refresh" button actually ran. The three numbers
// (newly / cached / deleted) together explain whether the doc
// changed or just had its timestamp bumped.
this._view?.webview.postMessage({
type: 'architectureRefreshResult',
value: {
projectName: profile.projectName,
docPath: result.docPath,
newlyAnalyzed: result.refreshStats.newlyAnalyzed,
cached: result.refreshStats.cached,
deleted: result.refreshStats.deleted.length,
durationMs: Date.now() - startedAt,
},
});
}
/**
* Re-attach the architecture context for the active project after a
* prior Detach. Rebuilds the doc (so the user gets a fresh scan),
* flips `architectureAutoAttach=true`, re-registers the watcher, and
* broadcasts the chip back to its active state. The complement of
* `_detachArchitecture`.
*/
async _attachArchitecture(): Promise<void> {
// `_ensureActiveProjectForWorkspace` guarantees the active project
// matches the current VS Code workspace — without that, hitting
// Attach right after opening a different folder would silently
// attach to whatever was last active in the *previous* workspace.
const profile = await this._ensureActiveProjectForWorkspace();
if (!profile || !profile.projectRoot) {
this._view?.webview.postMessage({
type: 'architectureRefreshFailed',
value: { reason: 'no-active-project' },
});
return;
}
await this._activateArchitectureForProject(profile.projectId, {
fallbackName: profile.projectName,
fallbackRoot: profile.projectRoot,
});
}
/**
* Make sure the active chronicle project actually corresponds to the
* folder the user has open in VS Code. Three cases:
*
* 1. Active project already matches workspace → return it as-is.
* 2. A *different* chronicle project matches the workspace → flip
* the active id to that one (the user switched folders since
* last session).
* 3. No chronicle project matches → synthesise a new one from the
* workspace folder name + register it.
*
* Returns the (possibly newly created) active project, or `null` when
* no workspace is open. Idempotent — calling repeatedly with no change
* is free.
*/
async _ensureActiveProjectForWorkspace(): Promise<ProjectProfile | null> {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) return null;
const projects = this._getChronicleProjects();
const active = this._getActiveChronicleProject();
const norm = (p: string | undefined) => (p || '').replace(/[\\/]+$/, '').toLowerCase();
if (active && active.projectRoot && norm(active.projectRoot) === norm(workspaceRoot)) {
return active;
}
// Case 2: another chronicle project matches → switch active to it
const matching = projects.find((p) => norm(p.projectRoot) === norm(workspaceRoot));
if (matching) {
await this._context.globalState.update(
SidebarChatProvider.activeChronicleProjectStateKey,
matching.projectId,
);
logInfo('architecture: switched active project to match workspace.', {
from: active?.projectId,
to: matching.projectId,
});
return matching;
}
// Case 3: synthesise a fresh entry for this workspace
const projectName = path.basename(workspaceRoot) || 'Current Project';
const projectId = this._slugify(projectName);
const now = new Date().toISOString();
const profile: ProjectProfile = {
projectId,
projectName,
projectRoot: workspaceRoot,
recordRoot: path.join(workspaceRoot, 'docs', 'records', projectName),
description: 'Auto-detected from workspace folder.',
corePurpose: '',
detailLevel: 'standard',
createdAt: now,
updatedAt: now,
};
const nextProjects = projects.filter((p) => p.projectId !== projectId).concat(profile);
await this._putChronicleProjects(nextProjects);
await this._context.globalState.update(
SidebarChatProvider.activeChronicleProjectStateKey,
projectId,
);
logInfo('architecture: registered new project from workspace.', {
projectId, projectRoot: workspaceRoot,
});
return profile;
}
/**
@@ -1166,24 +1279,84 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
});
}
/** Webview chip data — shown above the input box when active. */
/**
* Webview chip data. Three states:
*
* 1. **active** — project mode is on; doc is being auto-attached.
* 2. **inactive** — there's a project + workspace, but architecture
* is either never-activated or user-detached.
* The chip shows an `[Attach]` button instead of
* hiding entirely, so users always have a one-
* click path back into project mode.
* 3. **hidden** — no workspace open and no project at all.
*
* Also does an auto-activation pass for the *fresh-workspace* case:
* when the active project has no `architectureDocPath` yet AND the
* user hasn't explicitly detached, we generate the doc + flip
* `autoAttach=true` so the user opens a new folder and immediately
* sees the architecture context working. Existing detach choices are
* always respected.
*/
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
? {
// Always sync the active project to the current VS Code workspace
// before reporting — otherwise switching workspaces leaves the
// chip pointing at the *previous* project's doc.
const p = await this._ensureActiveProjectForWorkspace();
if (!p) {
this._view.webview.postMessage({ type: 'architectureStatus', value: { active: false } });
return;
}
const wasDetached = p.architectureAutoAttach === false;
const hasDoc = !!(p.architectureDocPath && fs.existsSync(p.architectureDocPath));
// Auto-activation for fresh workspaces: never been activated AND
// never been detached → kick off a build and re-broadcast. Single
// recursion is safe because the post-activate state will hit the
// `active` branch below.
if (!hasDoc && !wasDetached && p.projectRoot) {
try {
await this._activateArchitectureForProject(p.projectId, {
fallbackName: p.projectName,
fallbackRoot: p.projectRoot,
});
return; // _activateArchitectureForProject sends its own status
} catch (e: any) {
logError('architecture: auto-activate failed.', { error: e?.message ?? String(e) });
// Fall through to the inactive state so the user still sees an Attach button.
}
}
const fullyActive = hasDoc && !wasDetached;
if (fullyActive) {
this._view.webview.postMessage({
type: 'architectureStatus',
value: {
active: true,
projectId: p.projectId,
projectName: p.projectName,
docPath: p.architectureDocPath,
lastUpdated: p.architectureLastUpdated || '',
autoUpdate: p.architectureAutoUpdate !== false,
}
: { active: false },
});
},
});
// Re-register the watcher in case it was disposed (e.g. workspace switch).
this._registerArchitectureWatcher(p);
} else {
// Inactive but attachable: surface the project name + an Attach hook.
this._view.webview.postMessage({
type: 'architectureStatus',
value: {
active: false,
canAttach: !!p.projectRoot,
projectId: p.projectId,
projectName: p.projectName,
// Distinguishes "never activated" from "detached" so the
// chip can choose the right label ("Activate" vs "Re-attach").
detached: wasDetached,
},
});
}
}
// ─── 1인 기업 (Company) Mode ────────────────────────────────────────────