From ab8f9aa894f9fe4dc40e3f564410e4a042513022 Mon Sep 17 00:00:00 2001 From: hutiefang Date: Mon, 22 Jun 2026 09:07:28 +0800 Subject: [PATCH] fix(view): count nested task files --- src/utils/task-progress.ts | 29 +++++++++++++++++++++++++---- test/core/view.test.ts | 26 +++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/utils/task-progress.ts b/src/utils/task-progress.ts index a14b866f0..00b55b1dc 100644 --- a/src/utils/task-progress.ts +++ b/src/utils/task-progress.ts @@ -25,19 +25,40 @@ export function countTasksFromContent(content: string): TaskProgress { } export async function getTaskProgressForChange(changesDir: string, changeName: string): Promise { - 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 { + 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`; } - diff --git a/test/core/view.test.ts b/test/core/view.test.ts index b8b56df1e..4286a193c 100644 --- a/test/core/view.test.ts +++ b/test/core/view.test.ts @@ -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'); + }); +});