diff --git a/bmad-copilot/src/bmadIndex.ts b/bmad-copilot/src/bmadIndex.ts index a48a35073..0a6b8d8f7 100644 --- a/bmad-copilot/src/bmadIndex.ts +++ b/bmad-copilot/src/bmadIndex.ts @@ -33,6 +33,10 @@ export interface BmadIndex { // ─── Detection ────────────────────────────────────────── +/** + * Return the filesystem path of the first open workspace folder. + * @returns Absolute path, or `undefined` if no folder is open. + */ function workspaceRoot(): string | undefined { return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; } @@ -146,6 +150,12 @@ function extractWorkflowMeta(content: string): { name?: string; description?: st // ─── Recursive file walker ────────────────────────────── +/** + * Recursively collect all file paths under {@link dir}. + * Silently skips directories that cannot be read. + * @param dir - Root directory to walk. + * @returns Array of absolute file paths. + */ function walkAll(dir: string): string[] { const results: string[] = []; function walk(current: string): void { @@ -170,20 +180,43 @@ function walkAll(dir: string): string[] { // ─── Name derivation ──────────────────────────────────── +/** + * Test whether a filename matches the BMAD agent naming convention (`*.agent.yaml` / `*.agent.md`). + * @param filename - Base filename (no directory component). + * @returns `true` if the file is an agent definition. + */ function isAgentFile(filename: string): boolean { return /\.agent\.(yaml|md)$/i.test(filename); } +/** + * Test whether a filename matches the BMAD workflow naming convention + * (`workflow.yaml`, `workflow.md`, `workflow-*.yaml`, `workflow-*.md`). + * @param filename - Base filename (no directory component). + * @returns `true` if the file is a workflow definition. + */ function isWorkflowFile(filename: string): boolean { // Matches: workflow.yaml, workflow.md, workflow-*.md, workflow-*.yaml return /^workflow(-[\w-]+)?\.(yaml|md)$/i.test(filename); } +/** + * Derive a short display name from an agent file path by stripping the `.agent.yaml/.md` suffix. + * @param filePath - Absolute path to the agent file. + * @returns Display name (e.g. `"analyst"`). + */ function deriveAgentName(filePath: string): string { const base = path.basename(filePath); return base.replace(/\.agent\.(yaml|md)$/i, ''); } +/** + * Derive a display name for a workflow. Prefers the `name:` key from YAML + * metadata; falls back to stripping the `workflow-` prefix and extension. + * @param filePath - Absolute path to the workflow file. + * @param content - Raw file content (used for metadata extraction). + * @returns Display name (e.g. `"create-prd"`). + */ function deriveWorkflowName(filePath: string, content: string): string { // Prefer name from YAML metadata const meta = extractWorkflowMeta(content); @@ -209,6 +242,12 @@ function inferModule(filePath: string, bmadRoot: string): string | undefined { // ─── Build index ──────────────────────────────────────── +/** + * Walk the BMAD root directory and build a complete index of agents and workflows. + * Reads each matching file to extract metadata. Deduplicates workflows by name. + * @param bmadRoot - Absolute path to the detected BMAD root directory. + * @returns A {@link BmadIndex} with sorted agent and workflow lists. + */ export function buildIndex(bmadRoot: string): BmadIndex { const wsRoot = workspaceRoot() ?? bmadRoot; const allFiles = walkAll(bmadRoot); @@ -267,10 +306,18 @@ export function buildIndex(bmadRoot: string): BmadIndex { let _index: BmadIndex | undefined; let _watcher: vscode.FileSystemWatcher | undefined; +/** + * Return the current in-memory BMAD index without rebuilding. + * @returns The cached index, or `undefined` if not yet built. + */ export function getIndex(): BmadIndex | undefined { return _index; } +/** + * Re-detect the BMAD root and rebuild the index from disk. + * @returns The newly built index, or `undefined` if no BMAD root was found. + */ export function refreshIndex(): BmadIndex | undefined { const root = detectBmadRoot(); if (!root) { @@ -281,6 +328,11 @@ export function refreshIndex(): BmadIndex | undefined { return _index; } +/** + * Register a file-system watcher that rebuilds the BMAD index when + * YAML/Markdown files in the workspace change. Changes are debounced (500 ms). + * @param ctx - Extension context for disposable management. + */ export function startWatching(ctx: vscode.ExtensionContext): void { const wsRoot = workspaceRoot(); if (!wsRoot) { return; } diff --git a/bmad-copilot/src/chatHandler.ts b/bmad-copilot/src/chatHandler.ts index 417cadd22..34e19ec56 100644 --- a/bmad-copilot/src/chatHandler.ts +++ b/bmad-copilot/src/chatHandler.ts @@ -6,6 +6,11 @@ import { logInfo, logWarn, logError } from './logger'; // ─── Dynamic help builder ─────────────────────────────── +/** + * Build dynamic help Markdown that lists all slash commands and currently installed items. + * @param idx - Current BMAD index (may be `undefined` if not yet detected). + * @returns Formatted Markdown string. + */ function buildHelpText(idx: BmadIndex | undefined): string { let md = `## BMAD Copilot — Available Commands\n\n`; md += `| Command | Description |\n`; @@ -42,6 +47,11 @@ function buildHelpText(idx: BmadIndex | undefined): string { const KNOWN_SUBS = ['help', 'doctor', 'list', 'run', 'agents', 'workflows']; +/** + * Suggest the closest known slash command for a mistyped input using positional character overlap. + * @param input - The unrecognised command string the user typed. + * @returns The best-matching known command name (falls back to `"help"`). + */ function suggestCommand(input: string): string { let best = ''; let bestScore = 0; @@ -57,11 +67,19 @@ function suggestCommand(input: string): string { // ─── Sub-command handlers ─────────────────────────────── +/** + * Handle the `/help` command — render dynamic help text. + * @param stream - Chat response stream to write Markdown into. + */ function handleHelp(stream: vscode.ChatResponseStream): void { const idx = getIndex() ?? refreshIndex(); stream.markdown(buildHelpText(idx)); } +/** + * Handle the `/doctor` command — render BMAD installation diagnostics. + * @param stream - Chat response stream to write Markdown into. + */ function handleDoctor(stream: vscode.ChatResponseStream): void { const wsRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '(no workspace)'; const config = vscode.workspace.getConfiguration('bmad'); @@ -102,6 +120,11 @@ function handleDoctor(stream: vscode.ChatResponseStream): void { stream.markdown(md); } +/** + * Handle the `/list` command — display agents or workflows in a table. + * @param stream - Chat response stream. + * @param args - Remaining tokens (expects `["agents"]` or `["workflows"]`). + */ function handleList(stream: vscode.ChatResponseStream, args: string[]): void { const idx = getIndex() ?? refreshIndex(); if (!idx) { @@ -119,6 +142,13 @@ function handleList(stream: vscode.ChatResponseStream, args: string[]): void { } } +/** + * Render a Markdown table of BMAD items (agents or workflows). + * @param stream - Chat response stream. + * @param title - Section heading (e.g. `"Agents"`). + * @param items - Items to display. + * @param isAgent - `true` to show agent-specific columns (Title, Icon); `false` for workflow columns (Description). + */ function listItems(stream: vscode.ChatResponseStream, title: string, items: BmadItem[], isAgent: boolean): void { if (items.length === 0) { stream.markdown(`## ${title}\n\n_No items found._\n`); @@ -141,6 +171,12 @@ function listItems(stream: vscode.ChatResponseStream, title: string, items: Bmad stream.markdown(md); } +/** + * Truncate a string to at most {@link max} characters, appending `'…'` if shortened. + * @param s - Input string. + * @param max - Maximum allowed length. + * @returns The (possibly truncated) string. + */ function truncate(s: string, max: number): string { return s.length > max ? s.slice(0, max - 1) + '…' : s; } @@ -169,6 +205,14 @@ function safeFence(content: string): string { // ─── Run agent / workflow ─────────────────────────────── +/** + * Execute a resolved run target by injecting its definition into the LLM context. + * Falls back to displaying a copyable prompt if the Language Model API is unavailable. + * @param request - Original chat request (provides the language model). + * @param stream - Chat response stream for output. + * @param token - Cancellation token. + * @param resolved - The agent/workflow and task to execute. + */ async function executeRun( request: vscode.ChatRequest, stream: vscode.ChatResponseStream, @@ -221,6 +265,15 @@ async function executeRun( fallbackPrompt(stream, item, kind, fileContent, task); } +/** + * Handle the `/run` command — resolve args to an agent/workflow and execute it. + * Shows helpful feedback with closest-name suggestions on resolution failure. + * @param request - Original chat request. + * @param stream - Chat response stream. + * @param token - Cancellation token. + * @param args - Parsed tokens after `/run`. + * @returns A promise that resolves when execution is complete. + */ function handleRun( request: vscode.ChatRequest, stream: vscode.ChatResponseStream, @@ -253,6 +306,15 @@ function handleRun( return executeRun(request, stream, token, resolved); } +/** + * Display the assembled prompt as a copyable code block when the LLM API is unavailable. + * Includes a "Copy Prompt" button bound to the clipboard command. + * @param stream - Chat response stream. + * @param item - The agent or workflow item. + * @param kind - `"agent"` or `"workflow"`. + * @param fileContent - Raw content of the definition file. + * @param task - User-supplied task description. + */ function fallbackPrompt( stream: vscode.ChatResponseStream, item: BmadItem, @@ -275,6 +337,10 @@ function fallbackPrompt( // ─── Main handler ─────────────────────────────────────── +/** + * Main `@bmad` chat participant request handler. + * Routes incoming commands to the appropriate sub-handler and returns an empty result. + */ export const chatHandler: vscode.ChatRequestHandler = async ( request: vscode.ChatRequest, context: vscode.ChatContext, diff --git a/bmad-copilot/src/commandParser.ts b/bmad-copilot/src/commandParser.ts index 4b22d4c10..59a586870 100644 --- a/bmad-copilot/src/commandParser.ts +++ b/bmad-copilot/src/commandParser.ts @@ -128,10 +128,24 @@ export function resolveDirectKind(kind: ResolvedKind, args: string[], index: Bma // ── Helpers ── +/** + * Test whether a token looks like a BMAD module name (e.g. `"bmm"`, `"core"`). + * Rejects known command keywords so they are not confused with modules. + * @param s - Lowercased token to check. + * @returns `true` if the token is a plausible module name. + */ function isModuleName(s: string): boolean { return /^[a-z][a-z0-9_-]*$/.test(s) && !['agent', 'workflow', 'a', 'w', 'agents', 'workflows'].includes(s); } +/** + * Look up a named item of the given kind in the index. + * @param kind - `'agent'` or `'workflow'`. + * @param args - Remaining tokens: `[name, ...taskWords]`. + * @param index - Current BMAD index. + * @param moduleFilter - Optional module name to narrow the search. + * @returns Resolved target, or `undefined` if no match is found. + */ function findItem(kind: ResolvedKind, args: string[], index: BmadIndex, moduleFilter?: string): ResolvedRunTarget | undefined { if (args.length === 0) { return undefined; } const name = args[0].toLowerCase(); @@ -146,6 +160,12 @@ function findItem(kind: ResolvedKind, args: string[], index: BmadIndex, moduleFi return { kind, item, task }; } +/** + * Auto-resolve a name to an agent or workflow (agent takes priority). + * @param name - Item name to look up (case-insensitive). + * @param index - Current BMAD index. + * @returns The matched kind and item, or `undefined`. + */ function findByName(name: string, index: BmadIndex): { kind: ResolvedKind; item: BmadItem } | undefined { const n = name.toLowerCase(); const agent = index.agents.find(a => a.name.toLowerCase() === n); diff --git a/bmad-copilot/src/extension.ts b/bmad-copilot/src/extension.ts index 90e5d65b8..2d28aace8 100644 --- a/bmad-copilot/src/extension.ts +++ b/bmad-copilot/src/extension.ts @@ -3,6 +3,11 @@ import { initLogger, logInfo } from './logger'; import { refreshIndex, startWatching } from './bmadIndex'; import { chatHandler } from './chatHandler'; +/** + * Extension entry point — registers the `@bmad` chat participant, + * builds the initial BMAD index, and wires up file/config watchers. + * @param context - VS Code extension context for lifecycle management. + */ export function activate(context: vscode.ExtensionContext): void { // ── Logger ── initLogger(context); @@ -45,6 +50,7 @@ export function activate(context: vscode.ExtensionContext): void { logInfo('BMAD Copilot extension activated.'); } +/** Extension teardown. Cleanup is handled automatically by `context.subscriptions`. */ export function deactivate(): void { // Cleanup handled by context.subscriptions } diff --git a/bmad-copilot/src/logger.ts b/bmad-copilot/src/logger.ts index ffd36fa50..73c9c5f80 100644 --- a/bmad-copilot/src/logger.ts +++ b/bmad-copilot/src/logger.ts @@ -3,24 +3,42 @@ import * as vscode from 'vscode'; const CHANNEL_NAME = 'BMAD Copilot'; let _channel: vscode.OutputChannel | undefined; +/** + * Initialise the shared output channel for extension logging. + * @param ctx - Extension context whose subscriptions manage the channel lifecycle. + * @returns The created {@link vscode.OutputChannel}. + */ export function initLogger(ctx: vscode.ExtensionContext): vscode.OutputChannel { _channel = vscode.window.createOutputChannel(CHANNEL_NAME); ctx.subscriptions.push(_channel); return _channel; } +/** @returns Current time as `HH:MM:SS.mmm` for log line prefixes. */ function ts(): string { return new Date().toISOString().slice(11, 23); } +/** + * Write an INFO-level message to the output channel. + * @param msg - The message to log. + */ export function logInfo(msg: string): void { _channel?.appendLine(`[${ts()}] INFO ${msg}`); } +/** + * Write a WARN-level message to the output channel. + * @param msg - The message to log. + */ export function logWarn(msg: string): void { _channel?.appendLine(`[${ts()}] WARN ${msg}`); } +/** + * Write an ERROR-level message to the output channel. + * @param msg - The message to log. + */ export function logError(msg: string): void { _channel?.appendLine(`[${ts()}] ERROR ${msg}`); }