release: v2.0.6 - Intelligence & UX Optimization (2026-05-14)
This commit is contained in:
+184
-11
@@ -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 ────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user