= {
Rejected: 'Rejected',
Failed: 'Failed',
Expired: 'Expired',
+ Dismissed: 'Dismissed',
}
function reviewStatusLabel(status: ApiProposal['status']): string {
@@ -499,6 +515,34 @@ function applyBoardFilter(boardId: string) {
}
}
+const dismissableProposalIds = computed(() =>
+ proposals.value
+ .filter((p) => {
+ const status = normalizeProposalStatus(p.status)
+ return status === 'Applied' || status === 'Rejected' || status === 'Failed' || status === 'Expired'
+ })
+ .filter((p) => matchesActiveBoardFilter(p.boardId))
+ .map((p) => p.id),
+)
+
+async function handleDismissApplied() {
+ const ids = dismissableProposalIds.value
+ if (ids.length === 0) {
+ toast.info('No completed proposals to clear.')
+ return
+ }
+
+ try {
+ const result = await automationApi.dismissProposals(ids)
+ // Remove dismissed proposals from the local list
+ const dismissedSet = new Set(ids)
+ proposals.value = proposals.value.filter((p) => !dismissedSet.has(p.id))
+ toast.success(`Cleared ${result.dismissed} completed proposal${result.dismissed === 1 ? '' : 's'}.`)
+ } catch (e: unknown) {
+ toast.error(getErrorDisplay(e, 'Failed to clear proposals').message)
+ }
+}
+
function clearBoardFilter() {
boardFilterInput.value = ''
void router.push({ name: 'workspace-review' })
@@ -552,6 +596,17 @@ watch(
+
+
@@ -774,6 +829,29 @@ watch(
flex-wrap: wrap;
gap: var(--td-space-2);
justify-content: flex-end;
+ align-items: center;
+}
+
+.td-review__toggle {
+ display: flex;
+ align-items: center;
+ gap: var(--td-space-2);
+ cursor: pointer;
+ user-select: none;
+}
+
+.td-review__toggle-input {
+ accent-color: var(--td-color-primary);
+ width: 16px;
+ height: 16px;
+ cursor: pointer;
+}
+
+.td-review__toggle-label {
+ font-size: var(--td-font-sm);
+ font-weight: 600;
+ color: var(--td-text-secondary);
+ white-space: nowrap;
}
.td-review__summary {
diff --git a/frontend/taskdeck-web/tests/e2e/automation-ops.spec.ts b/frontend/taskdeck-web/tests/e2e/automation-ops.spec.ts
index 5cb07aba1..350e84262 100644
--- a/frontend/taskdeck-web/tests/e2e/automation-ops.spec.ts
+++ b/frontend/taskdeck-web/tests/e2e/automation-ops.spec.ts
@@ -1,10 +1,10 @@
-import type { APIRequestContext } from '@playwright/test'
-import { expect, test } from '@playwright/test'
-import { parseTrueishEnv } from '../../scripts/demo-shared.mjs'
-import { API_BASE_URL, registerAndAttachSession, type AuthResult } from './support/authSession'
-import { createBoardWithColumn } from './support/boardHelpers'
-import { assertOk } from './support/httpAsserts'
-import { pollUntil } from './support/polling'
+import type { APIRequestContext } from '@playwright/test'
+import { expect, test } from '@playwright/test'
+import { parseTrueishEnv } from '../../scripts/demo-shared.mjs'
+import { API_BASE_URL, registerAndAttachSession, type AuthResult } from './support/authSession'
+import { createBoardWithColumn } from './support/boardHelpers'
+import { assertOk } from './support/httpAsserts'
+import { pollUntil } from './support/polling'
interface ChatMessageDto {
proposalId: string | null
@@ -50,28 +50,28 @@ test.beforeEach(async ({ page, request }) => {
auth = await registerAndAttachSession(page, request, 'ops')
})
-test('chat session should create and return assistant response', async ({ page }) => {
- await page.goto('/workspace/automations/chat')
- const expectLiveProvider = parseTrueishEnv(process.env.TASKDECK_RUN_LIVE_LLM_TESTS)
-
- if (expectLiveProvider) {
- await expect(page.locator('[data-llm-health-state="configured"]')).toBeVisible()
- await expect(page.getByText('Live LLM configured')).toBeVisible()
- } else {
- await expect(page.locator('[data-llm-health-state="mock"]')).toBeVisible()
- await expect(page.getByText('Live LLM not active')).toBeVisible()
- }
-
- await page.getByPlaceholder('Session title').fill(`Session ${Date.now()}`)
- await page.getByRole('button', { name: 'Create Session' }).click()
+test('chat session should create and return assistant response', async ({ page }) => {
+ await page.goto('/workspace/automations/chat')
+ const expectLiveProvider = parseTrueishEnv(process.env.TASKDECK_RUN_LIVE_LLM_TESTS)
+
+ if (expectLiveProvider) {
+ await expect(page.locator('[data-llm-health-state="configured"]')).toBeVisible()
+ await expect(page.getByText('Live LLM configured')).toBeVisible()
+ } else {
+ await expect(page.locator('[data-llm-health-state="mock"]')).toBeVisible()
+ await expect(page.getByText('Live LLM not active')).toBeVisible()
+ }
+
+ await page.getByPlaceholder('Session title').fill(`Session ${Date.now()}`)
+ await page.getByRole('button', { name: 'Create Session' }).click()
await expect(page.getByText('Session', { exact: false }).first()).toBeVisible()
await page.getByPlaceholder('Describe an automation instruction...').fill('summarize this board status')
await page.getByRole('button', { name: 'Send Message' }).click()
-
- await expect(page.getByText('Assistant').first()).toBeVisible()
-})
+
+ await expect(page.getByText('Assistant').first()).toBeVisible()
+})
test('ops cli should run health.check template', async ({ page }) => {
await page.goto('/workspace/ops/cli')
@@ -120,7 +120,7 @@ test('chat proposal flow should create, approve, and execute proposal', async ({
const proposal = await proposalResponse.json() as ProposalDto
await page.goto('/workspace/review')
- await expect(page.getByRole('heading', { name: 'Review', exact: true })).toBeVisible()
+ await expect(page.getByRole('heading', { name: 'Review', exact: true })).toBeVisible()
const proposalCard = page.locator('.td-review-card').filter({ hasText: proposal.summary }).first()
await expect(proposalCard).toBeVisible()
@@ -130,5 +130,5 @@ test('chat proposal flow should create, approve, and execute proposal', async ({
page.once('dialog', (dialog) => dialog.accept())
await proposalCard.getByRole('button', { name: 'Apply to board' }).click()
- await expect(proposalCard.getByText('Applied')).toBeVisible()
+ await expect(proposalCard).not.toBeVisible()
})
diff --git a/frontend/taskdeck-web/tests/e2e/capture-loop.spec.ts b/frontend/taskdeck-web/tests/e2e/capture-loop.spec.ts
index e68f715ef..45fe0eea6 100644
--- a/frontend/taskdeck-web/tests/e2e/capture-loop.spec.ts
+++ b/frontend/taskdeck-web/tests/e2e/capture-loop.spec.ts
@@ -59,7 +59,7 @@ test('capture triage should create proposal and apply card with provenance links
page.once('dialog', (dialog) => dialog.accept())
await proposalCard.getByRole('button', { name: 'Apply to board' }).click()
- await expect(proposalCard.getByText('Applied')).toBeVisible()
+ await expect(proposalCard).not.toBeVisible()
const createdCard = await waitForCardWithTitle(request, auth, boardId, checklistTaskTitle)
diff --git a/frontend/taskdeck-web/tests/e2e/first-run.spec.ts b/frontend/taskdeck-web/tests/e2e/first-run.spec.ts
index f62abfd1f..a3cebe022 100644
--- a/frontend/taskdeck-web/tests/e2e/first-run.spec.ts
+++ b/frontend/taskdeck-web/tests/e2e/first-run.spec.ts
@@ -132,10 +132,13 @@ test('first-run path should guide home to capture to review to execute to board'
page.once('dialog', (dialog) => dialog.accept())
await proposalCard.getByRole('button', { name: 'Apply to board' }).click()
- await expect(proposalCard.getByText('Applied')).toBeVisible()
+ await expect(proposalCard).not.toBeVisible()
const createdCard = await waitForCardWithTitle(request, auth, boardId, cardTitle)
+ // Toggle "Show completed" to reveal applied proposal and its Open Board button
+ await page.locator('.td-review__toggle-input').check()
+ await expect(proposalCard).toBeVisible()
await proposalCard.getByRole('button', { name: 'Open Board' }).click()
await expect(page).toHaveURL(new RegExp(`/workspace/boards/${boardId}$`))
const card = page.locator('[data-card-id]').filter({ hasText: createdCard.title }).first()
diff --git a/frontend/taskdeck-web/tests/e2e/manual-audit.spec.ts b/frontend/taskdeck-web/tests/e2e/manual-audit.spec.ts
index e64666726..06f6ce5f0 100644
--- a/frontend/taskdeck-web/tests/e2e/manual-audit.spec.ts
+++ b/frontend/taskdeck-web/tests/e2e/manual-audit.spec.ts
@@ -109,7 +109,7 @@ test.describe('Core loop: Home -> Inbox/Capture -> Review -> Board', () => {
// Step 9: Apply proposal to board
page.once('dialog', (dialog) => dialog.accept())
await proposalCard.getByRole('button', { name: 'Apply to board' }).click()
- await expect(proposalCard.getByText('Applied')).toBeVisible()
+ await expect(proposalCard).not.toBeVisible()
await page.screenshot({ path: testInfo.outputPath('06-review-applied.png'), fullPage: true })
// Step 10: Verify card on board