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
23 changes: 16 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"postinstall": "node ./node_modules/vscode/bin/install"
},
"devDependencies": {
"@types/node": "^24.9.1",
"@types/vscode": "^1.105.0",
"copyfiles": "^2.4.1",
"typescript": "^4.0.0",
"vscode": "^1.1.36",
Expand Down
12 changes: 11 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as vscode from 'vscode';
import { GitExtension, Repository } from './git';
import { getDiffData, openDifftool } from './utils/gitUtils';
import { getCodeRefinements, suggestComment, ProjectDetails, generateFileStructure } from './utils/geminiUtils';
import { getCodeRefinements, suggestComment, ProjectDetails, generateFileStructure, generateDirectoryOverviews } from './utils/geminiUtils';
import { updateFilesInWorkspace, executeCommand, makeFileStructure } from './utils/terminalUtils';
import { fetchAllRepos } from './utils/githubUtils';
import { techStackData } from './assets/techStackData';
import { updateReadmes } from './utils/readmeUtils';

async function initializeProject() {
const name = await vscode.window.showInputBox({
Expand Down Expand Up @@ -147,6 +148,15 @@ async function onFilesStaged() {
const shouldIncludeDescription = selection === hauntWithDescButton;
await updateFilesInWorkspace(refinedCode, shouldIncludeDescription);

// --- generate directory READMEs ---
progress.report({ message: "Generating directory README overviews..." });
try {
const dirOverviews = await generateDirectoryOverviews(refinedCode);
await updateReadmes(dirOverviews);
} catch (e: any) {
console.warn('Failed to generate directory overviews:', e?.message || e);
}

// --- openDiffTool ---
progress.report({ message: "Opening difftool for your review..." });
await openDifftool();
Expand Down
57 changes: 57 additions & 0 deletions src/utils/geminiUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,61 @@ async function conventionalCommitPrompt(files: GitDiffData[]): Promise<string> {
const finalPrompt = promptTemplate.replace('{{diffDataAsJson}}', diffAsJson);

return finalPrompt;
}

export async function generateDirectoryOverviews(refinedFiles: RefinedCode[]): Promise<Record<string, string>> {
const apiKey = key();
const geminiUrl = url();

if (!Array.isArray(refinedFiles) || refinedFiles.length === 0) {
return {};
}

// Group files by directory
const byDir: Record<string, { files: Array<{ name: string; desc: string }> }> = {};
for (const f of refinedFiles) {
if (!f || typeof f.name !== 'string') continue;
const dir = path.posix.dirname(f.name).replace(/^\.$/, '.');
if (!byDir[dir]) byDir[dir] = { files: [] };
byDir[dir].files.push({ name: path.posix.basename(f.name), desc: f.desc });
}

const promptTemplate = await getCustomPrompt('generateDirectoryOverview');
const directoriesJson = JSON.stringify(byDir, null, 2);
const finalPrompt = promptTemplate.replace('{{directoriesJson}}', directoriesJson);

const response = await fetch(geminiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-goog-api-key': apiKey,
},
body: JSON.stringify({
contents: [{ parts: [{ text: finalPrompt }] }],
generationConfig: {
responseMimeType: 'application/json',
},
}),
});

if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Gemini API request failed with status ${response.status}: ${errorBody}`);
}

const data = await response.json() as any;
const responseText = data?.candidates?.[0]?.content?.parts?.[0]?.text;
if (!responseText) {
throw new Error('No valid content found in Gemini API response.');
}

const errors: any[] = [];
const parsed = parse(responseText, errors) as Record<string, string> | undefined;
if (errors.length > 0) {
console.warn('JSONC parser encountered recoverable errors while parsing directory overviews:', errors);
}
if (!parsed || typeof parsed !== 'object') {
throw new Error('Failed to parse directory overview JSON from API response.');
}
return parsed;
}
16 changes: 16 additions & 0 deletions src/utils/promptUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface CustomPrompts {
suggestComment?: string;
getCodeRefinements?: string;
generateFileStructure?: string;
generateDirectoryOverview?: string;
}

const defaultPrompts = {
Expand Down Expand Up @@ -103,6 +104,21 @@ Generate a JSON object representing the project's file structure.
1. **JSON ONLY:** Your entire response must be a single, raw JSON object.
2. **NO MARKDOWN:** Do not wrap the JSON in markdown code blocks like \`\`\`json.
3. **NO EXTRA TEXT:** Do not include ANY text, headers, footers, explanations, or conversational filler before or after the JSON object. Your response must start with { and end with }.
`
,
generateDirectoryOverview: `
You are an expert technical writer assisting in maintaining internal project documentation.

Task: Given a list of directories with their files and each file's high-level description, produce a concise overview for every directory that accurately reflects its purpose and contents.

Output Requirements:
- Respond with a single JSON object mapping directory paths to a short paragraph summary string.
- Keep each summary under 120 words.
- Be specific but succinct; mention notable files, responsibilities, and how the directory fits in the project when inferable from the file descriptions.
- Do NOT include code fences or any text other than the raw JSON object.

Input Data:
{{directoriesJson}}
`
};

Expand Down
85 changes: 85 additions & 0 deletions src/utils/readmeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as vscode from 'vscode';

const AUTO_HEADER = '## Directory Overview (Auto-generated)';

function buildAutoSection(summary: string): string {
return `${AUTO_HEADER}\n\n${summary.trim()}\n`;
}

async function readFileIfExists(uri: vscode.Uri): Promise<string | undefined> {
try {
const raw = await vscode.workspace.fs.readFile(uri);
return Buffer.from(raw).toString('utf8');
} catch {
return undefined;
}
}

function upsertAutoSection(existing: string | undefined, summary: string): string {
const autoSection = buildAutoSection(summary);

if (!existing || existing.trim().length === 0) {
// Create a minimal README with just the auto section under a basic title
return `# README\n\n${autoSection}`;
}

const headerRegex = /^##\s+Directory Overview \(Auto-generated\)\s*$/gim;
const nextHeaderRegex = /^##\s+/gim;

const match = headerRegex.exec(existing);
if (match && typeof match.index === 'number') {
const startOfHeader = match.index;
const endOfHeader = startOfHeader + match[0].length;

// Find the next level-2 header after the auto header to know replacement bounds
nextHeaderRegex.lastIndex = endOfHeader;
const nextHeaderMatch = nextHeaderRegex.exec(existing);
const replaceEnd = nextHeaderMatch ? nextHeaderMatch.index : existing.length;

const before = existing.slice(0, startOfHeader);
const after = existing.slice(replaceEnd);
const needsNewlineBefore = !before.endsWith('\n\n') ? (before.endsWith('\n') ? '\n' : '\n\n') : '';
const needsNewlineAfter = after.startsWith('\n') ? '' : '\n';
return `${before}${needsNewlineBefore}${autoSection}${needsNewlineAfter}${after}`;
}

// If no existing auto section, append one at the end, ensuring spacing
const needsNewline = existing.endsWith('\n') ? '' : '\n';
const needsDouble = existing.endsWith('\n\n') ? '' : (existing.endsWith('\n') ? '\n' : '\n\n');
return `${existing}${needsDouble}${autoSection}${needsNewline}`;
}

export async function upsertReadmeForDirectory(dirRelativePath: string, summary: string): Promise<void> {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
throw new Error('No open project folder found.');
}
const rootUri = workspaceFolder.uri;

const dirUri = dirRelativePath === '.'
? rootUri
: vscode.Uri.joinPath(rootUri, dirRelativePath);
const readmeUri = vscode.Uri.joinPath(dirUri, 'README.md');

// Ensure directory exists before writing
await vscode.workspace.fs.createDirectory(dirUri);

const existing = await readFileIfExists(readmeUri);
const updated = upsertAutoSection(existing, summary);
const bytes = Buffer.from(updated, 'utf8');
await vscode.workspace.fs.writeFile(readmeUri, bytes);
}

export async function updateReadmes(summaries: Record<string, string>): Promise<void> {
for (const [dir, summary] of Object.entries(summaries)) {
try {
await upsertReadmeForDirectory(dir || '.', summary);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`Failed to update README for ${dir}:`, message);
// Continue with other directories
}
}
}

export const README_AUTO_HEADER = AUTO_HEADER;
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["esnext"],
"lib": ["esnext", "dom"],
"types": ["node", "vscode"]
},
"include": ["src/**/*.ts"],
Expand Down