diff --git a/src/components/shared/left-panel-new.tsx b/src/components/shared/left-panel-new.tsx
index 735da53..69daf07 100644
--- a/src/components/shared/left-panel-new.tsx
+++ b/src/components/shared/left-panel-new.tsx
@@ -51,6 +51,8 @@ const views = [
{ id: 'graph', label: 'Graph' },
] as const;
+const SHORT_DESCRIPTION_MIN_LENGTH = 200;
+
function isTaskMatch(task: BeadIssue, filters: LeftPanelFilters): boolean {
if (filters.query.trim()) {
const query = filters.query.toLowerCase();
@@ -80,6 +82,10 @@ function isTaskMatch(task: BeadIssue, filters: LeftPanelFilters): boolean {
if (task.status === 'closed' || task.status === 'tombstone') return false;
}
+ const isEpic = task.issue_type === 'epic';
+ if (!isEpic && filters.hideNoAcceptance && !task.acceptance_criteria?.trim()) return false;
+ if (!isEpic && filters.hideShortDescription && (task.description ?? '').length < SHORT_DESCRIPTION_MIN_LENGTH) return false;
+
return true;
}
@@ -321,6 +327,26 @@ export function LeftPanel({
>
Hide Closed
+
+
diff --git a/src/components/shared/left-panel.tsx b/src/components/shared/left-panel.tsx
index 14607cd..2591388 100644
--- a/src/components/shared/left-panel.tsx
+++ b/src/components/shared/left-panel.tsx
@@ -19,8 +19,12 @@ export interface LeftPanelFilters {
priority: LeftPanelPriorityFilter;
preset: LeftPanelPresetFilter;
hideClosed: boolean;
+ hideNoAcceptance: boolean;
+ hideShortDescription: boolean;
}
+export const SHORT_DESCRIPTION_MIN_LENGTH = 200;
+
export interface LeftPanelProps {
issues: BeadIssue[];
selectedEpicId?: string | null;
@@ -185,8 +189,11 @@ function rowTone(entry: EpicEntry): string {
return 'var(--surface-tertiary)';
}
-function isTaskMatch(task: BeadIssue, filters: LeftPanelFilters): boolean {
+export function isTaskMatch(task: BeadIssue, filters: LeftPanelFilters): boolean {
if (filters.hideClosed && (task.status === 'closed' || task.status === 'tombstone')) return false;
+ const isEpic = task.issue_type === 'epic';
+ if (!isEpic && filters.hideNoAcceptance && !task.acceptance_criteria?.trim()) return false;
+ if (!isEpic && filters.hideShortDescription && (task.description ?? '').length < SHORT_DESCRIPTION_MIN_LENGTH) return false;
const normalizedQuery = filters.query.trim().toLowerCase();
if (normalizedQuery.length > 0) {
const searchable = `${task.id} ${task.title} ${task.labels.join(' ')}`.toLowerCase();
@@ -333,6 +340,26 @@ export function LeftPanel({
>
Hide Closed
+
+
diff --git a/src/components/shared/unified-shell.tsx b/src/components/shared/unified-shell.tsx
index 7e3cee5..a267128 100644
--- a/src/components/shared/unified-shell.tsx
+++ b/src/components/shared/unified-shell.tsx
@@ -67,6 +67,8 @@ export function UnifiedShell({
priority: 'all',
preset: 'all',
hideClosed: true,
+ hideNoAcceptance: true,
+ hideShortDescription: true,
});
const [actor, setActor] = useState('');
diff --git a/src/hooks/use-url-state.ts b/src/hooks/use-url-state.ts
index 1513341..400eb8a 100644
--- a/src/hooks/use-url-state.ts
+++ b/src/hooks/use-url-state.ts
@@ -8,8 +8,18 @@ export type PanelState = 'open' | 'closed';
export type DrawerState = 'open' | 'closed';
export type GraphTabType = 'flow' | 'overview';
export type LeftSidebarMode = 'epics' | 'orchestrator';
-export type LeftPanelStatusFilter = 'all' | 'ready' | 'in_progress' | 'blocked' | 'deferred' | 'done';export type LeftPanelPriorityFilter = 'all' | 'P0' | 'P1' | 'P2' | 'P3' | 'P4';export type LeftPanelPresetFilter = 'all' | 'active' | 'blocked_agents';export interface LeftPanelFilters { status: LeftPanelStatusFilter; priority: LeftPanelPriorityFilter; preset: LeftPanelPresetFilter; hideClosed: boolean;
- query: string;}
+export type LeftPanelStatusFilter = 'all' | 'ready' | 'in_progress' | 'blocked' | 'deferred' | 'done';
+export type LeftPanelPriorityFilter = 'all' | 'P0' | 'P1' | 'P2' | 'P3' | 'P4';
+export type LeftPanelPresetFilter = 'all' | 'active' | 'blocked_agents';
+export interface LeftPanelFilters {
+ status: LeftPanelStatusFilter;
+ priority: LeftPanelPriorityFilter;
+ preset: LeftPanelPresetFilter;
+ hideClosed: boolean;
+ query: string;
+ hideNoAcceptance: boolean;
+ hideShortDescription: boolean;
+}
export interface UrlState {
view: ViewType;
diff --git a/src/lib/parser.ts b/src/lib/parser.ts
index 5d6a434..d90390d 100644
--- a/src/lib/parser.ts
+++ b/src/lib/parser.ts
@@ -58,6 +58,7 @@ function normalizeIssue(raw: ParseableBeadIssue): BeadIssue {
due_at: typeof raw.due_at === 'string' ? raw.due_at : null,
estimated_minutes: typeof raw.estimated_minutes === 'number' ? raw.estimated_minutes : null,
external_ref: typeof raw.external_ref === 'string' ? raw.external_ref : null,
+ acceptance_criteria: typeof raw.acceptance_criteria === 'string' ? raw.acceptance_criteria : null,
metadata: typeof raw.metadata === 'object' && raw.metadata !== null ? (raw.metadata as Record) : {},
};
}
diff --git a/src/lib/read-issues-dolt.ts b/src/lib/read-issues-dolt.ts
index ffccabe..407adf4 100644
--- a/src/lib/read-issues-dolt.ts
+++ b/src/lib/read-issues-dolt.ts
@@ -8,6 +8,7 @@ interface IssueRow extends RowDataPacket {
id: string;
title: string;
description: string | null;
+ acceptance_criteria: string | null;
status: string;
priority: number;
issue_type: string;
@@ -62,6 +63,7 @@ function normalizeRow(row: IssueRow, deps: BeadDependency[]): BeadIssue {
estimated_minutes: typeof row.estimated_minutes === 'number' ? row.estimated_minutes : null,
external_ref: row.external_ref ?? null,
comments_count: (row.comments_count ?? 0) as number,
+ acceptance_criteria: row.acceptance_criteria ?? null,
metadata: row.metadata ?? {},
};
}
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 0c8ae60..c528307 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -58,6 +58,8 @@ export interface BeadIssue {
agentTypeId?: string;
/** Which specific agent instance is assigned (if running) */
agentInstanceId?: string;
+ /** Free-form acceptance criteria captured by bd create --acceptance */
+ acceptance_criteria?: string | null;
metadata: Record;
}
diff --git a/tests/components/shared/left-panel-filtering.test.ts b/tests/components/shared/left-panel-filtering.test.ts
index e714e88..40ce99a 100644
--- a/tests/components/shared/left-panel-filtering.test.ts
+++ b/tests/components/shared/left-panel-filtering.test.ts
@@ -1,7 +1,8 @@
import test from 'node:test';
import assert from 'node:assert/strict';
-import { shouldHideEpicEntry, type LeftPanelFilters } from '../../../src/components/shared/left-panel';
+import { isTaskMatch, shouldHideEpicEntry, type LeftPanelFilters } from '../../../src/components/shared/left-panel';
+import type { BeadIssue } from '../../../src/lib/types';
const defaultFilters: LeftPanelFilters = {
query: '',
@@ -9,8 +10,38 @@ const defaultFilters: LeftPanelFilters = {
priority: 'all',
preset: 'all',
hideClosed: true,
+ hideNoAcceptance: true,
+ hideShortDescription: true,
};
+function makeIssue(overrides: Partial = {}): BeadIssue {
+ return {
+ id: 'test-1',
+ title: 'Test issue',
+ description: 'x'.repeat(400),
+ status: 'open',
+ priority: 2,
+ issue_type: 'task',
+ assignee: null,
+ templateId: null,
+ owner: null,
+ labels: [],
+ dependencies: [],
+ created_at: '2026-01-01T00:00:00Z',
+ updated_at: '2026-01-01T00:00:00Z',
+ closed_at: null,
+ close_reason: null,
+ closed_by_session: null,
+ created_by: null,
+ due_at: null,
+ estimated_minutes: null,
+ external_ref: null,
+ acceptance_criteria: 'Some acceptance criteria text',
+ metadata: {},
+ ...overrides,
+ };
+}
+
test('does not hide epics with no children when hideClosed is the only active toggle', () => {
const hidden = shouldHideEpicEntry({
epicStatus: 'open',
@@ -82,3 +113,50 @@ test('hides closed selected epic when hideClosed is enabled', () => {
assert.equal(hidden, true);
});
+
+test('hideNoAcceptance=true hides task with empty acceptance_criteria', () => {
+ const task = makeIssue({ acceptance_criteria: '' });
+ assert.equal(isTaskMatch(task, defaultFilters), false);
+});
+
+test('hideNoAcceptance=true hides task with whitespace-only acceptance_criteria', () => {
+ const task = makeIssue({ acceptance_criteria: ' \n\t ' });
+ assert.equal(isTaskMatch(task, defaultFilters), false);
+});
+
+test('hideNoAcceptance=false shows task with empty acceptance_criteria', () => {
+ const task = makeIssue({ acceptance_criteria: null });
+ const filters = { ...defaultFilters, hideNoAcceptance: false };
+ assert.equal(isTaskMatch(task, filters), true);
+});
+
+test('hideNoAcceptance=true keeps epic visible regardless of acceptance_criteria', () => {
+ const epic = makeIssue({ issue_type: 'epic', acceptance_criteria: null });
+ assert.equal(isTaskMatch(epic, defaultFilters), true);
+});
+
+test('hideShortDescription=true hides task with 199-character description', () => {
+ const task = makeIssue({ description: 'x'.repeat(199) });
+ assert.equal(isTaskMatch(task, defaultFilters), false);
+});
+
+test('hideShortDescription=true shows task with 200-character description', () => {
+ const task = makeIssue({ description: 'x'.repeat(200) });
+ assert.equal(isTaskMatch(task, defaultFilters), true);
+});
+
+test('hideShortDescription=false shows task with short description', () => {
+ const task = makeIssue({ description: 'short' });
+ const filters = { ...defaultFilters, hideShortDescription: false, hideNoAcceptance: false };
+ assert.equal(isTaskMatch(task, filters), true);
+});
+
+test('hideShortDescription=true keeps epic visible regardless of description length', () => {
+ const epic = makeIssue({ issue_type: 'epic', description: 'short' });
+ assert.equal(isTaskMatch(epic, defaultFilters), true);
+});
+
+test('hideShortDescription treats null description as length 0', () => {
+ const task = makeIssue({ description: null });
+ assert.equal(isTaskMatch(task, defaultFilters), false);
+});