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
29 changes: 25 additions & 4 deletions src/utils/task-progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,40 @@ export function countTasksFromContent(content: string): TaskProgress {
}

export async function getTaskProgressForChange(changesDir: string, changeName: string): Promise<TaskProgress> {
const tasksPath = path.join(changesDir, changeName, 'tasks.md');
const changeDir = path.join(changesDir, changeName);
try {
const content = await fs.readFile(tasksPath, 'utf-8');
return countTasksFromContent(content);
const taskFiles = await findTaskFiles(changeDir);
const progress = { total: 0, completed: 0 };
for (const taskFile of taskFiles) {
const content = await fs.readFile(taskFile, 'utf-8');
const fileProgress = countTasksFromContent(content);
progress.total += fileProgress.total;
progress.completed += fileProgress.completed;
}
return progress;
} catch {
return { total: 0, completed: 0 };
}
}

async function findTaskFiles(dir: string): Promise<string[]> {
const taskFiles: string[] = [];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
taskFiles.push(...await findTaskFiles(entryPath));
}
else if (entry.isFile() && entry.name === 'tasks.md') {
taskFiles.push(entryPath);
}
}
return taskFiles.sort();
}

export function formatTaskStatus(progress: TaskProgress): string {
if (progress.total === 0) return 'No tasks';
if (progress.completed === progress.total) return '✓ Complete';
return `${progress.completed}/${progress.total} tasks`;
}


26 changes: 25 additions & 1 deletion test/core/view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,29 @@ describe('ViewCommand', () => {
'gamma-change'
]);
});
});

it('counts nested tasks files when rendering change progress', async () => {
const changesDir = path.join(tempDir, 'openspec', 'changes');
const changeDir = path.join(changesDir, 'layered-change');
await fs.mkdir(path.join(changeDir, 'backend'), { recursive: true });
await fs.mkdir(path.join(changeDir, 'frontend'), { recursive: true });

await fs.writeFile(
path.join(changeDir, 'backend', 'tasks.md'),
'- [x] Add backend endpoint\n- [ ] Add backend tests\n'
);
await fs.writeFile(
path.join(changeDir, 'frontend', 'tasks.md'),
'- [ ] Wire frontend view\n'
);

const viewCommand = new ViewCommand();
await viewCommand.execute(tempDir);

const output = logOutput.map(stripAnsi).join('\n');

expect(output).toContain('Active Changes');
expect(output).toContain('Task Progress: 1/3 (33% complete)');
expect(output).toContain('◉ layered-change');
});
});