From 6c2362c0d7968dc810b414a23ad0c818e2393ee4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 08:49:21 +0000 Subject: [PATCH 1/2] Initial plan From 4ad533882b5b7f52d41934205b8aba2b0d735da0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 08:58:29 +0000 Subject: [PATCH 2/2] feat: show risk and effort scores in metadata pane and CLI output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Risk and Effort rows to MetadataPaneComponent (with '—' placeholder when empty) - Always show Risk/Effort in CLI concise/normal/full/tree formats (with '—' placeholder) - Update tests: row count 9→11, add risk/effort rendering assertions - Add new tests for placeholder and value rendering in TUI and CLI helpers - Update CLI.md and TUI.md docs to mention Risk/Effort always visible Co-authored-by: SorraTheOrc <250240+SorraTheOrc@users.noreply.github.com> --- CLI.md | 2 + TUI.md | 1 + src/commands/helpers.ts | 14 ++++--- src/tui/components/metadata-pane.ts | 5 +++ tests/cli/helpers-tree-rendering.test.ts | 50 ++++++++++++++++++++++++ tests/tui/tui-50-50-layout.test.ts | 10 +++-- tests/tui/tui-github-metadata.test.ts | 37 +++++++++++++++--- 7 files changed, 104 insertions(+), 15 deletions(-) diff --git a/CLI.md b/CLI.md index 6d298a6..8ca2c9a 100644 --- a/CLI.md +++ b/CLI.md @@ -212,6 +212,8 @@ Options: `-c, --children` — Also display descendants in a tree layout (optional). `--prefix ` (optional) +The output always includes `Risk` and `Effort` fields. When a field has no value a placeholder `—` is shown so the field is consistently visible for triage and prioritization. + Examples: ```sh diff --git a/TUI.md b/TUI.md index 88560b2..81fb074 100644 --- a/TUI.md +++ b/TUI.md @@ -7,6 +7,7 @@ This document describes the interactive terminal UI shipped as the `wl tui` (or - The TUI presents a tree view of work items on the left and a details pane on the right. - It can show all items, or be limited to in-progress items via `--in-progress`. - The details pane uses the same human formatter as the CLI so what you see in the TUI matches `wl show --format full`. +- The metadata pane (top-right) shows Status, Stage, Priority, Risk, Effort, Comments, Tags, Assignee, Created, Updated, and GitHub link for the selected item. `Risk` and `Effort` always appear; when a field has no value a placeholder `—` is shown. - Integrated OpenCode AI assistant for intelligent work item management and coding assistance. ## Controls diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index 0e055e8..ad2b384 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -124,8 +124,8 @@ export function displayItemTree(items: WorkItem[]): void { ? `Status: ${item.status} · Stage: ${effectiveStage} | Priority: ${item.priority}` : `Status: ${item.status} | Priority: ${item.priority}`; console.log(`${detailIndent}${statusSummary}`); - if (item.risk) console.log(`${detailIndent}Risk: ${item.risk}`); - if (item.effort) console.log(`${detailIndent}Effort: ${item.effort}`); + console.log(`${detailIndent}Risk: ${item.risk || '—'}`); + console.log(`${detailIndent}Effort: ${item.effort || '—'}`); if (item.assignee) console.log(`${detailIndent}Assignee: ${item.assignee}`); if (item.tags.length > 0) console.log(`${detailIndent}Tags: ${item.tags.join(', ')}`); } @@ -257,8 +257,8 @@ export function humanFormatWorkItem(item: WorkItem, db: WorklogDatabase | null, lines.push(`Status: ${statusLabel} | Priority: ${item.priority}`); } lines.push(sortIndexLabel); - if (item.risk) lines.push(`Risk: ${item.risk}`); - if (item.effort) lines.push(`Effort: ${item.effort}`); + lines.push(`Risk: ${item.risk || '—'}`); + lines.push(`Effort: ${item.effort || '—'}`); if (item.assignee) lines.push(`Assignee: ${item.assignee}`); if (item.tags && item.tags.length > 0) lines.push(`Tags: ${item.tags.join(', ')}`); return lines.join('\n'); @@ -277,8 +277,8 @@ export function humanFormatWorkItem(item: WorkItem, db: WorklogDatabase | null, lines.push(`Status: ${statusLabel} | Priority: ${item.priority}`); } lines.push(sortIndexLabel); - if (item.risk) lines.push(`Risk: ${item.risk}`); - if (item.effort) lines.push(`Effort: ${item.effort}`); + lines.push(`Risk: ${item.risk || '—'}`); + lines.push(`Effort: ${item.effort || '—'}`); if (item.assignee) lines.push(`Assignee: ${item.assignee}`); if (item.parentId) lines.push(`Parent: ${item.parentId}`); if (item.description) lines.push(`Description: ${item.description}`); @@ -323,7 +323,9 @@ export function humanFormatWorkItem(item: WorkItem, db: WorklogDatabase | null, ['SortIndex', String(item.sortIndex)] ]; if (item.risk) frontmatter.push(['Risk', item.risk]); + else frontmatter.push(['Risk', '—']); if (item.effort) frontmatter.push(['Effort', item.effort]); + else frontmatter.push(['Effort', '—']); if (item.assignee) frontmatter.push(['Assignee', item.assignee]); if (item.parentId) frontmatter.push(['Parent', item.parentId]); if (item.tags && item.tags.length > 0) frontmatter.push(['Tags', item.tags.join(', ')]); diff --git a/src/tui/components/metadata-pane.ts b/src/tui/components/metadata-pane.ts index d87d2f1..c3085f6 100644 --- a/src/tui/components/metadata-pane.ts +++ b/src/tui/components/metadata-pane.ts @@ -64,6 +64,8 @@ export class MetadataPaneComponent { status?: string; stage?: string; priority?: string; + risk?: string; + effort?: string; tags?: string[]; assignee?: string; createdAt?: Date | string; @@ -75,10 +77,13 @@ export class MetadataPaneComponent { this.box.setContent(''); return; } + const placeholder = '—'; const lines: string[] = []; lines.push(`Status: ${item.status ?? ''}`); lines.push(`Stage: ${item.stage ?? ''}`); lines.push(`Priority: ${item.priority ?? ''}`); + lines.push(`Risk: ${item.risk && item.risk.trim() ? item.risk : placeholder}`); + lines.push(`Effort: ${item.effort && item.effort.trim() ? item.effort : placeholder}`); lines.push(`Comments: ${commentCount}`); lines.push(`Tags: ${item.tags && item.tags.length > 0 ? item.tags.join(', ') : ''}`); lines.push(`Assignee: ${item.assignee ?? ''}`); diff --git a/tests/cli/helpers-tree-rendering.test.ts b/tests/cli/helpers-tree-rendering.test.ts index 84e7ead..04b75b3 100644 --- a/tests/cli/helpers-tree-rendering.test.ts +++ b/tests/cli/helpers-tree-rendering.test.ts @@ -105,4 +105,54 @@ describe('tree rendering helpers', () => { expect(childBIndex).toBeGreaterThanOrEqual(0); expect(childBIndex).toBeLessThan(childAIndex); }); + + it('shows Risk and Effort placeholders when fields are empty', () => { + const item = baseWorkItem({ id: 'TEST-RISK-1', title: 'Risk Test', risk: '', effort: '' }); + + const { lines, spy } = captureConsole(); + displayItemTree([item]); + spy.mockRestore(); + + const normalized = lines.map(stripAnsi); + expect(normalized.some(line => line.includes('Risk: —'))).toBe(true); + expect(normalized.some(line => line.includes('Effort: —'))).toBe(true); + }); + + it('shows Risk and Effort values when set', () => { + const item = baseWorkItem({ id: 'TEST-RISK-2', title: 'Risk Set', risk: 'High', effort: 'M' }); + + const { lines, spy } = captureConsole(); + displayItemTree([item]); + spy.mockRestore(); + + const normalized = lines.map(stripAnsi); + expect(normalized.some(line => line.includes('Risk: High'))).toBe(true); + expect(normalized.some(line => line.includes('Effort: M'))).toBe(true); + }); + + it('shows Risk and Effort in concise format output', () => { + const item = baseWorkItem({ id: 'TEST-RISK-3', title: 'Concise Test', risk: 'Low', effort: 'XS' }); + const items = [item]; + + const { lines, spy } = captureConsole(); + displayItemTreeWithFormat(items, null, 'concise'); + spy.mockRestore(); + + const normalized = lines.map(stripAnsi).join('\n'); + expect(normalized).toContain('Risk: Low'); + expect(normalized).toContain('Effort: XS'); + }); + + it('shows Risk and Effort placeholders in normal format when fields are empty', () => { + const item = baseWorkItem({ id: 'TEST-RISK-4', title: 'Normal Test', risk: '', effort: '' }); + const items = [item]; + + const { lines, spy } = captureConsole(); + displayItemTreeWithFormat(items, null, 'normal'); + spy.mockRestore(); + + const normalized = lines.map(stripAnsi).join('\n'); + expect(normalized).toContain('Risk: —'); + expect(normalized).toContain('Effort: —'); + }); }); diff --git a/tests/tui/tui-50-50-layout.test.ts b/tests/tui/tui-50-50-layout.test.ts index 1c23d08..ee01d3b 100644 --- a/tests/tui/tui-50-50-layout.test.ts +++ b/tests/tui/tui-50-50-layout.test.ts @@ -450,6 +450,8 @@ describe('TUI 50/50 split layout', () => { expect(capturedContent).toContain('in-progress'); expect(capturedContent).toContain('Priority:'); expect(capturedContent).toContain('high'); + expect(capturedContent).toContain('Risk:'); + expect(capturedContent).toContain('Effort:'); expect(capturedContent).toContain('Comments: 3'); expect(capturedContent).toContain('Tags:'); expect(capturedContent).toContain('backend'); @@ -460,11 +462,11 @@ describe('TUI 50/50 split layout', () => { expect(capturedContent).toContain('Jan 1, 2024'); expect(capturedContent).toContain('Updated:'); expect(capturedContent).toContain('Jun 1, 2024'); - // GitHub row is always present (9th row) + // GitHub row is always present (11th row) expect(capturedContent).toContain('GitHub:'); // All rows should always be present (consistent layout) const lines = capturedContent.split('\n'); - expect(lines.length).toBe(9); + expect(lines.length).toBe(11); }); it('MetadataPaneComponent.updateFromItem clears content for null item', () => { @@ -518,9 +520,9 @@ describe('TUI 50/50 split layout', () => { assignee: '', }, 0); - // All 9 rows should always be present for consistent layout + // All 11 rows should always be present for consistent layout const lines = capturedContent.split('\n'); - expect(lines.length).toBe(9); + expect(lines.length).toBe(11); expect(capturedContent).toContain('Status:'); expect(capturedContent).toContain('Tags:'); expect(capturedContent).toContain('Assignee:'); diff --git a/tests/tui/tui-github-metadata.test.ts b/tests/tui/tui-github-metadata.test.ts index e2d5dce..bd517ae 100644 --- a/tests/tui/tui-github-metadata.test.ts +++ b/tests/tui/tui-github-metadata.test.ts @@ -103,20 +103,20 @@ describe('MetadataPaneComponent GitHub row', () => { expect(content).toContain('G to push'); }); - it('always renders exactly 9 rows regardless of GitHub state', () => { + it('always renders exactly 11 rows regardless of GitHub state', () => { const { comp, getContent } = createMockMetadataPane(); // With no github fields comp.updateFromItem({ status: 'open' }, 0); - expect(getContent().split('\n').length).toBe(9); + expect(getContent().split('\n').length).toBe(11); // With github mapping comp.updateFromItem({ status: 'open', githubRepo: 'o/r', githubIssueNumber: 1 }, 0); - expect(getContent().split('\n').length).toBe(9); + expect(getContent().split('\n').length).toBe(11); // With github configured but no mapping comp.updateFromItem({ status: 'open', githubRepo: 'o/r' }, 0); - expect(getContent().split('\n').length).toBe(9); + expect(getContent().split('\n').length).toBe(11); }); it('clears content for null item', () => { @@ -127,8 +127,35 @@ describe('MetadataPaneComponent GitHub row', () => { }); // --------------------------------------------------------------------------- -// Integration tests: controller passes github fields to updateFromItem +// Unit tests: MetadataPaneComponent Risk and Effort row rendering // --------------------------------------------------------------------------- +describe('MetadataPaneComponent Risk and Effort rows', () => { + it('shows placeholder when risk and effort are not set', () => { + const { comp, getContent } = createMockMetadataPane(); + comp.updateFromItem({ status: 'open' }, 0); + expect(getContent()).toContain('Risk:'); + expect(getContent()).toContain('Effort:'); + expect(getContent()).toMatch(/Risk:\s+—/); + expect(getContent()).toMatch(/Effort:\s+—/); + }); + + it('shows risk and effort values when set', () => { + const { comp, getContent } = createMockMetadataPane(); + comp.updateFromItem({ status: 'open', risk: 'High', effort: 'M' }, 0); + const content = getContent(); + expect(content).toMatch(/Risk:\s+High/); + expect(content).toMatch(/Effort:\s+M/); + }); + + it('shows placeholder when risk and effort are empty strings', () => { + const { comp, getContent } = createMockMetadataPane(); + comp.updateFromItem({ status: 'open', risk: '', effort: '' }, 0); + const content = getContent(); + expect(content).toMatch(/Risk:\s+—/); + expect(content).toMatch(/Effort:\s+—/); + }); +}); + describe('TUI metadata pane receives GitHub fields', () => { it('calls updateFromItem after start', async () => { const ctx = createTuiTestContext();