Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
12 changes: 11 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import * as vscode from "vscode";
import { createCompareBranchCommand } from "./commands/compareBranch";
import { createCompareFileWithBranchCommand } from "./commands/compareFileWithBranch";
import { createDrawFlowChartCommand } from "./commands/drawFlowChart";
import { createExplainRepoCommand } from "./commands/explainRepo";
import { createExplainSelectionCommand } from "./commands/explainSelection";
import { createGeneratePrDescriptionCommand } from "./commands/generatePrDescription";
import { handlePanelAction, openFileRef } from "./commands/shared";
import { createTraceRelationshipsCommand } from "./commands/traceRelationships";
import { CachedResultEntry } from "./models/types";
import { BranchAnalysisService } from "./services/analysis/BranchAnalysisService";
import { FlowAnalysisService } from "./services/analysis/FlowAnalysisService";
import { PrDescriptionAnalysisService } from "./services/analysis/PrDescriptionAnalysisService";
import { RepoAnalysisService } from "./services/analysis/RepoAnalysisService";
import { SelectionAnalysisService } from "./services/analysis/SelectionAnalysisService";
import { GitHubService } from "./services/github/GitHubService";
import { PromptBuilder } from "./services/llm/PromptBuilder";
import { RelationshipService } from "./services/repo/RelationshipService";
import { RepoScanner } from "./services/repo/RepoScanner";
Expand All @@ -22,10 +26,12 @@ export function activate(context: vscode.ExtensionContext): void {
const repoScanner = new RepoScanner();
const symbolService = new SymbolService();
const relationshipService = new RelationshipService(symbolService);
const githubService = new GitHubService();
const promptBuilder = new PromptBuilder();
const repoAnalysis = new RepoAnalysisService(repoScanner, promptBuilder);
const branchAnalysis = new BranchAnalysisService(repoScanner, promptBuilder);
const flowAnalysis = new FlowAnalysisService(repoScanner, promptBuilder);
const prDescriptionAnalysis = new PrDescriptionAnalysisService(repoScanner, promptBuilder, githubService);
const selectionAnalysis = new SelectionAnalysisService(
repoScanner,
symbolService,
Expand All @@ -38,7 +44,9 @@ export function activate(context: vscode.ExtensionContext): void {
const explainRepo = createExplainRepoCommand(panel, repoAnalysis, cacheService, sidebarProvider);
const explainSelection = createExplainSelectionCommand(panel, selectionAnalysis, cacheService, sidebarProvider);
const compareBranch = createCompareBranchCommand(panel, branchAnalysis, cacheService, sidebarProvider);
const compareFileWithBranch = createCompareFileWithBranchCommand(panel, branchAnalysis, cacheService, sidebarProvider);
const drawFlowChart = createDrawFlowChartCommand(panel, flowAnalysis, cacheService, sidebarProvider);
const generatePrDescription = createGeneratePrDescriptionCommand(panel, prDescriptionAnalysis);
const traceRelationships = createTraceRelationshipsCommand(
panel,
selectionAnalysis,
Expand All @@ -52,7 +60,9 @@ export function activate(context: vscode.ExtensionContext): void {
vscode.commands.registerCommand("codeExplainer.explainRepo", wrapCommand(explainRepo)),
vscode.commands.registerCommand("codeExplainer.explainSelection", wrapCommand(explainSelection)),
vscode.commands.registerCommand("codeExplainer.compareBranch", wrapCommand(compareBranch)),
vscode.commands.registerCommand("codeExplainer.compareFileWithBranch", wrapCommand(compareFileWithBranch)),
vscode.commands.registerCommand("codeExplainer.drawFlowChart", wrapCommand(drawFlowChart)),
vscode.commands.registerCommand("codeExplainer.generatePrDescription", wrapCommand(generatePrDescription)),
vscode.commands.registerCommand("codeExplainer.traceRelationships", wrapCommand(traceRelationships)),
vscode.commands.registerCommand(
"codeExplainer.openCachedResult",
Expand All @@ -63,7 +73,7 @@ export function activate(context: vscode.ExtensionContext): void {
}

panel.show(entry.result, {
onAction: (action) => void handlePanelAction(action),
onAction: (action) => void handlePanelAction(action, panel),
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: () =>
void refreshCachedEntry(
Expand Down
Loading