Skip to content
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Code Explainer is a VS Code extension scaffold for branch-aware code comprehensi
- Compare the current branch with `main`
- Explain the current selection with surrounding context
- Trace relationships between files, symbols, tests, and configuration
- Generate a reviewable GitHub PR title and description for the current branch

## Development

Expand Down
34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"onCommand:codeExplainer.explainRepo",
"onCommand:codeExplainer.explainSelection",
"onCommand:codeExplainer.compareBranch",
"onCommand:codeExplainer.generatePrDescription",
"onCommand:codeExplainer.traceRelationships",
"onCommand:codeExplainer.drawFlowChart",
"onView:codeExplainer.sidebar"
Expand All @@ -38,6 +39,10 @@
"command": "codeExplainer.traceRelationships",
"title": "Code Explainer: Trace Relationships"
},
{
"command": "codeExplainer.generatePrDescription",
"title": "Code Explainer: Generate PR Description"
},
{
"command": "codeExplainer.drawFlowChart",
"title": "Code Explainer: Draw Current Branch Diagram"
Expand Down Expand Up @@ -77,12 +82,20 @@
{
"command": "codeExplainer.drawFlowChart",
"group": "navigation@102"
},
{
"command": "codeExplainer.generatePrDescription",
"group": "navigation@103"
}
],
"explorer/context": [
{
"command": "codeExplainer.explainRepo",
"group": "navigation@100"
},
{
"command": "codeExplainer.generatePrDescription",
"group": "navigation@101"
}
]
},
Expand Down Expand Up @@ -114,6 +127,27 @@
"default": true,
"description": "Render custom flow-chart visualizations in the explanation panel when available."
},
"codeExplainer.prDescription.defaultStyle": {
"type": "string",
"enum": [
"business-stakeholder",
"code-collaborator",
"manager",
"other"
],
"default": "manager",
"description": "Default audience/style for generated PR descriptions."
},
"codeExplainer.prDescription.defaultGuidelines": {
"type": "string",
"default": "",
"description": "Optional default team guidance or house rules to apply when generating PR descriptions."
},
"codeExplainer.prDescription.defaultTemplate": {
"type": "string",
"default": "",
"description": "Optional default markdown template or preferred section structure for generated PR descriptions."
},
"codeExplainer.openai.apiKey": {
"type": "string",
"default": "",
Expand Down
2 changes: 1 addition & 1 deletion src/commands/compareBranch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function createCompareBranchCommand(
getFresh: () => analysisService.analyze(),
render: (result, refresh) =>
panel.show(result, {
onAction: (action) => void handlePanelAction(action),
onAction: (action) => void handlePanelAction(action, panel),
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: refresh,
}),
Expand Down
47 changes: 47 additions & 0 deletions src/commands/compareFileWithBranch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as path from "path";
import * as vscode from "vscode";
import { BranchAnalysisService } from "../services/analysis/BranchAnalysisService";
import { GitService } from "../services/git/GitService";
import { CacheService } from "../storage/CacheService";
import { CodeExplainerProvider } from "../ui/sidebar/CodeExplainerProvider";
import { ResultsPanel } from "../ui/webview/panel";
import { handlePanelAction, openFileRef } from "./shared";

export function createCompareFileWithBranchCommand(
panel: ResultsPanel,
analysisService: BranchAnalysisService,
cache: CacheService,
sidebarProvider: CodeExplainerProvider
) {
return async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
throw new Error("Open a file before comparing it with the base branch.");
}

const filePath = editor.document.uri.fsPath;
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;

if (!workspaceRoot) {
throw new Error("Open a workspace folder before comparing.");
}

const config = vscode.workspace.getConfiguration("codeExplainer");
const baseBranch = config.get<string>("baseBranch", "main");
const git = new GitService(workspaceRoot);

if (!(await git.isGitRepo())) {
throw new Error("The current workspace is not a git repository.");
}

// Get the changes for this specific file
const result = await analysisService.analyzeFile(filePath, baseBranch);
const fileName = path.basename(filePath);

panel.show(result, {
onAction: (action) => void handlePanelAction(action, panel),
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: () => void createCompareFileWithBranchCommand(panel, analysisService, cache, sidebarProvider)(),
});
};
}
2 changes: 1 addition & 1 deletion src/commands/drawFlowChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function createDrawFlowChartCommand(
getFresh: () => analysisService.analyze(),
render: (result, refresh) =>
panel.show(result, {
onAction: (action) => void handlePanelAction(action),
onAction: (action) => void handlePanelAction(action, panel),
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: refresh,
}),
Expand Down
63 changes: 63 additions & 0 deletions src/commands/explainDirectory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as path from "path";
import * as vscode from "vscode";
import { DirectoryAnalysisService } from "../services/analysis/DirectoryAnalysisService";
import { CacheService } from "../storage/CacheService";
import { CodeExplainerProvider } from "../ui/sidebar/CodeExplainerProvider";
import { ResultsPanel } from "../ui/webview/panel";
import { handlePanelAction, openFileRef, showCachedOrFresh } from "./shared";

export function createExplainDirectoryCommand(
panel: ResultsPanel,
analysisService: DirectoryAnalysisService,
cache: CacheService,
sidebarProvider: CodeExplainerProvider
) {
return async (uri?: vscode.Uri, forceRefresh = false) => {
let directoryPath: string;

if (uri) {
// Called from context menu on a folder
const stat = await vscode.workspace.fs.stat(uri);
if (stat.type === vscode.FileType.Directory) {
directoryPath = uri.fsPath;
} else {
// If it's a file, use its parent directory
directoryPath = path.dirname(uri.fsPath);
}
} else {
// Called from command palette - use active editor's directory
const editor = vscode.window.activeTextEditor;
if (!editor) {
throw new Error("Open a file or select a folder before using Explain Directory.");
}
directoryPath = path.dirname(editor.document.uri.fsPath);
}

const folder = vscode.workspace.workspaceFolders?.[0];
if (!folder) {
throw new Error("Open a workspace folder before using Code Explainer.");
}

const relativePath = path.relative(folder.uri.fsPath, directoryPath);
const dirName = path.basename(directoryPath);
const cacheKey = `directory:${folder.uri.fsPath}:${relativePath}`;

await showCachedOrFresh({
panel,
cache,
sidebarProvider,
cacheKey,
label: `Directory: ${dirName}`,
source: { kind: "directory", directoryPath },
loadingMessage: `Analyzing the ${dirName} directory.`,
forceRefresh,
getFresh: () => analysisService.analyze(directoryPath),
render: (result, refresh) =>
panel.show(result, {
onAction: (action) => void handlePanelAction(action, panel),
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: refresh,
}),
});
};
}
2 changes: 1 addition & 1 deletion src/commands/explainRepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function createExplainRepoCommand(
getFresh: () => analysisService.analyze(),
render: (result, refresh) =>
panel.show(result, {
onAction: (action) => void handlePanelAction(action),
onAction: (action) => void handlePanelAction(action, panel),
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: refresh,
}),
Expand Down
2 changes: 1 addition & 1 deletion src/commands/explainSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function createExplainSelectionCommand(
getFresh: () => analysisService.explainSelection({ filePath, startLine, endLine }),
render: (result, refresh) =>
panel.show(result, {
onAction: (action) => void handlePanelAction(action),
onAction: (action) => void handlePanelAction(action, panel),
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: refresh,
}),
Expand Down
106 changes: 106 additions & 0 deletions src/commands/generatePrDescription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as vscode from "vscode";
import { PrDescriptionExplanation, PrDescriptionStyle } from "../models/types";
import { PrDescriptionAnalysisService } from "../services/analysis/PrDescriptionAnalysisService";
import { ResultsPanel } from "../ui/webview/panel";
import { openFileRef } from "./shared";

type DraftPanelMessage =
| {
type: "prRegenerate";
title: string;
body: string;
style: PrDescriptionStyle;
customInstructions: string;
}
| {
type: "prApply";
title: string;
body: string;
style: PrDescriptionStyle;
customInstructions: string;
};

export function createGeneratePrDescriptionCommand(
panel: ResultsPanel,
analysisService: PrDescriptionAnalysisService
) {
return async () => {
const renderResult = (result: PrDescriptionExplanation) => {
panel.show(result, {
onAction: () => undefined,
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: () => {
void runAnalysis();
},
onMessage: (message) => {
void handlePanelMessage(message as DraftPanelMessage, result, renderResult);
},
});
};

const runAnalysis = async (options?: { style?: PrDescriptionStyle; customInstructions?: string }) => {
panel.showLoading("Generate PR Description", "Reviewing the current branch and preparing a PR description draft.");
const result = await analysisService.analyze(options);
renderResult(result);
};

const handlePanelMessage = async (
message: DraftPanelMessage,
currentResult: PrDescriptionExplanation,
render: (result: PrDescriptionExplanation) => void
) => {
if (message.type === "prRegenerate") {
await runAnalysis({
style: message.style,
customInstructions: message.customInstructions,
});
return;
}

if (message.type !== "prApply") {
return;
}

panel.showLoading("Generate PR Description", "Applying the reviewed draft to GitHub.");
const applied = await analysisService.applyDraft({
draft: currentResult,
title: message.title,
body: message.body,
});

if (applied.status === "cancelled") {
render({
...currentResult,
draftTitle: message.title,
draftBody: message.body,
style: message.style,
customInstructions: message.customInstructions,
});
return;
}

const updatedResult = analysisService.withAppliedDraft(
{
...currentResult,
style: message.style,
customInstructions: message.customInstructions,
},
message.title,
message.body,
applied.pullRequest
);
render(updatedResult);

const messageText = applied.status === "created"
? `Created pull request #${applied.pullRequest?.number}.`
: `Updated pull request #${applied.pullRequest?.number}.`;
const openAction = "Open PR";
const selection = await vscode.window.showInformationMessage(messageText, openAction);
if (selection === openAction && applied.pullRequest?.url) {
await vscode.env.openExternal(vscode.Uri.parse(applied.pullRequest.url));
}
};

await runAnalysis();
};
}
13 changes: 12 additions & 1 deletion src/commands/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import { CacheService } from "../storage/CacheService";
import { CodeExplainerProvider } from "../ui/sidebar/CodeExplainerProvider";
import { ResultsPanel } from "../ui/webview/panel";

export async function handlePanelAction(action: string): Promise<void> {
export async function handlePanelAction(action: string, panel?: ResultsPanel): Promise<void> {
const normalized = action.toLowerCase();

if (normalized.includes("branch")) {
// If we're in a selection context, compare only that file
const currentResult = panel?.getCurrentResult();
if (currentResult?.kind === "selection") {
await vscode.commands.executeCommand("codeExplainer.compareFileWithBranch");
return;
}
await vscode.commands.executeCommand("codeExplainer.compareBranch");
return;
}
Expand All @@ -33,6 +39,11 @@ export async function handlePanelAction(action: string): Promise<void> {
return;
}

if (normalized.includes("pr description")) {
await vscode.commands.executeCommand("codeExplainer.generatePrDescription");
return;
}

await vscode.commands.executeCommand("codeExplainer.explainRepo");
}

Expand Down
2 changes: 1 addition & 1 deletion src/commands/traceRelationships.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function createTraceRelationshipsCommand(
getFresh: () => analysisService.traceRelationships({ filePath, startLine, endLine }),
render: (result, refresh) =>
panel.show(result, {
onAction: (action) => void handlePanelAction(action),
onAction: (action) => void handlePanelAction(action, panel),
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: refresh,
}),
Expand Down
Loading