diff options
| author | Claudomator Agent <agent@claudomator> | 2026-03-16 00:56:16 +0000 |
|---|---|---|
| committer | Claudomator Agent <agent@claudomator> | 2026-03-16 00:56:16 +0000 |
| commit | 7f6254cdafc6143f80ee9ca8e482c36aff2c197e (patch) | |
| tree | 79219e0ba80ac67c7483568082ea9a951f4d386c | |
| parent | 908fb8650ce1f295b6047fe95a75ec4aa235b7dc (diff) | |
feat: replace static subtask placeholder with task description
When a BLOCKED/READY task has no subtasks yet, show the task description
(truncated to ~120 chars at a word boundary) instead of the generic
'Waiting for subtasksβ¦' text. Falls back to task.name if no description,
and finally to the original generic text if neither is present.
- Add truncateToWordBoundary(text, maxLen=120) helper
- Update renderSubtaskRollup(task, footer) to use task object instead of taskId
- Update both READY and BLOCKED call sites
- Add web/test/subtask-placeholder.test.mjs with 11 tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | web/app.js | 17 | ||||
| -rw-r--r-- | web/test/subtask-placeholder.test.mjs | 92 |
2 files changed, 104 insertions, 5 deletions
@@ -96,6 +96,12 @@ export function renderChangestatsBadge(stats, doc = (typeof document !== 'undefi return span; } +function truncateToWordBoundary(text, maxLen = 120) { + if (!text || text.length <= maxLen) return text; + const cut = text.lastIndexOf(' ', maxLen); + return (cut > 0 ? text.slice(0, cut) : text.slice(0, maxLen)) + 'β¦'; +} + function createTaskCard(task) { const card = document.createElement('div'); card.className = 'task-card'; @@ -175,7 +181,7 @@ function createTaskCard(task) { }); footer.appendChild(btn); } else if (task.state === 'READY') { - renderSubtaskRollup(task.id, footer); + renderSubtaskRollup(task, footer); const acceptBtn = document.createElement('button'); acceptBtn.className = 'btn-accept'; acceptBtn.textContent = 'Accept'; @@ -196,7 +202,7 @@ function createTaskCard(task) { if (task.question) { renderQuestionFooter(task, footer); } else { - renderSubtaskRollup(task.id, footer); + renderSubtaskRollup(task, footer); } const cancelBtn = document.createElement('button'); cancelBtn.className = 'btn-cancel'; @@ -867,17 +873,18 @@ const STATE_EMOJI = { READY: 'π', BLOCKED: 'βΈ', }; -async function renderSubtaskRollup(taskId, footer) { +async function renderSubtaskRollup(task, footer) { footer.addEventListener('click', (e) => e.stopPropagation()); const container = document.createElement('div'); container.className = 'subtask-rollup'; footer.prepend(container); try { - const res = await fetch(`${API_BASE}/api/tasks/${taskId}/subtasks`); + const res = await fetch(`${API_BASE}/api/tasks/${task.id}/subtasks`); const subtasks = await res.json(); if (!subtasks || subtasks.length === 0) { - container.textContent = 'Waiting for subtasksβ¦'; + const blurb = task.description || task.name; + container.textContent = blurb ? truncateToWordBoundary(blurb) : 'Waiting for subtasksβ¦'; return; } const ul = document.createElement('ul'); diff --git a/web/test/subtask-placeholder.test.mjs b/web/test/subtask-placeholder.test.mjs new file mode 100644 index 0000000..2449faa --- /dev/null +++ b/web/test/subtask-placeholder.test.mjs @@ -0,0 +1,92 @@ +// subtask-placeholder.test.mjs β placeholder text for empty subtask rollup +// +// Run with: node --test web/test/subtask-placeholder.test.mjs + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +// ββ Logic under test ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ +// +// When a task is BLOCKED/READY and the subtask list is empty, the rollup shows +// meaningful content instead of a generic placeholder: +// 1. task.description truncated to ~120 chars (word boundary) +// 2. fallback to task.name if no description +// 3. fallback to 'Waiting for subtasksβ¦' if neither + +function truncateToWordBoundary(text, maxLen = 120) { + if (!text || text.length <= maxLen) return text; + const cut = text.lastIndexOf(' ', maxLen); + return (cut > 0 ? text.slice(0, cut) : text.slice(0, maxLen)) + 'β¦'; +} + +function getSubtaskPlaceholder(task) { + const blurb = task.description || task.name; + return blurb ? truncateToWordBoundary(blurb) : 'Waiting for subtasksβ¦'; +} + +// ββ Tests βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ + +describe('truncateToWordBoundary', () => { + it('returns text unchanged when shorter than maxLen', () => { + assert.equal(truncateToWordBoundary('hello world', 120), 'hello world'); + }); + + it('returns text unchanged when exactly maxLen', () => { + const text = 'a'.repeat(120); + assert.equal(truncateToWordBoundary(text, 120), text); + }); + + it('truncates at word boundary and appends ellipsis', () => { + const text = 'Fix the login bug that causes users to be logged out unexpectedly when they navigate between pages in the application user interface'; + const result = truncateToWordBoundary(text, 120); + assert.ok(result.length <= 121, `result too long: ${result.length}`); + assert.ok(result.endsWith('β¦'), 'should end with ellipsis'); + assert.ok(result.startsWith('Fix the'), 'should keep beginning of text'); + }); + + it('truncates at character boundary when no word boundary exists', () => { + const text = 'a'.repeat(200); + const result = truncateToWordBoundary(text, 120); + assert.equal(result, 'a'.repeat(120) + 'β¦'); + }); + + it('returns empty string unchanged', () => { + assert.equal(truncateToWordBoundary(''), ''); + }); + + it('returns null unchanged', () => { + assert.equal(truncateToWordBoundary(null), null); + }); +}); + +describe('getSubtaskPlaceholder', () => { + it('uses task.description when available', () => { + const task = { description: 'Fix auth bug', name: 'auth-fix' }; + assert.equal(getSubtaskPlaceholder(task), 'Fix auth bug'); + }); + + it('truncates task.description at 120 chars', () => { + const longDesc = 'This is a very long description that exceeds the limit. ' + + 'It goes on and on describing what the task is about in great detail. ' + + 'Eventually it reaches the maximum allowed length.'; + const task = { description: longDesc, name: 'long-task' }; + const result = getSubtaskPlaceholder(task); + assert.ok(result.endsWith('β¦'), 'should end with ellipsis'); + assert.ok(result.length <= 122, `result too long: ${result.length}`); + }); + + it('falls back to task.name when description is absent', () => { + const task = { name: 'deploy-frontend' }; + assert.equal(getSubtaskPlaceholder(task), 'deploy-frontend'); + }); + + it('falls back to task.name when description is empty string', () => { + const task = { description: '', name: 'deploy-frontend' }; + assert.equal(getSubtaskPlaceholder(task), 'deploy-frontend'); + }); + + it('falls back to generic text when both description and name are absent', () => { + const task = {}; + assert.equal(getSubtaskPlaceholder(task), 'Waiting for subtasksβ¦'); + }); +}); |
