fix(HAI-039): eliminate duplicate description in prompts and modal heading

- Fix prompt heading to use only task ID when title is absent, preventing description duplication
- Strip leading markdown heading from TaskDetailModal since the modal has its own header
- Add store tests covering prompt generation for tasks with and without titles
- Update TaskDetailModal tests to verify heading stripping and single description rendering
This commit is contained in:
Dustin Byrne
2026-03-25 22:58:09 -04:00
parent 3c868666ab
commit 397716f370
4 changed files with 86 additions and 8 deletions

View File

@@ -55,6 +55,58 @@ describe("TaskStore", () => {
return task;
}
// ── Prompt generation (no duplicate description) ───────────────
describe("prompt generation", () => {
it("triage task without title does not duplicate description in PROMPT.md", async () => {
const task = await store.createTask({ description: "Fix the login bug" });
const detail = await store.getTask(task.id);
// Heading should be just the ID, not the description
expect(detail.prompt).toMatch(/^# HAI-001\n/);
// Description appears exactly once
const count = detail.prompt.split("Fix the login bug").length - 1;
expect(count).toBe(1);
});
it("triage task with title uses title in heading and description in body", async () => {
const task = await store.createTask({
title: "Login bug",
description: "Fix the login bug on the settings page",
});
const detail = await store.getTask(task.id);
expect(detail.prompt).toMatch(/^# HAI-001: Login bug\n/);
expect(detail.prompt).toContain("Fix the login bug on the settings page");
});
it("generateSpecifiedPrompt does not duplicate when title is absent", async () => {
const task = await store.createTask({
description: "Implement caching layer",
column: "todo",
});
const detail = await store.getTask(task.id);
// Heading should be just the ID
expect(detail.prompt).toMatch(/^# HAI-001\n/);
// Description appears exactly once (in Mission section)
const count = detail.prompt.split("Implement caching layer").length - 1;
expect(count).toBe(1);
});
it("generateSpecifiedPrompt uses title in heading when present", async () => {
const task = await store.createTask({
title: "Add caching",
description: "Implement caching layer for API responses",
column: "todo",
});
const detail = await store.getTask(task.id);
expect(detail.prompt).toMatch(/^# HAI-001: Add caching\n/);
expect(detail.prompt).toContain("Implement caching layer for API responses");
});
});
// ── Lock serialization test ──────────────────────────────────────
describe("write lock serialization", () => {

View File

@@ -197,9 +197,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
// Update cache if watcher is active
if (this.watcher) this.taskCache.set(id, { ...task });
const heading = task.title || task.description;
const heading = task.title ? `${id}: ${task.title}` : id;
const prompt = task.column === "triage"
? `# ${id}: ${heading}\n\n${task.description}\n`
? `# ${heading}\n\n${task.description}\n`
: this.generateSpecifiedPrompt(task);
await writeFile(join(dir, "PROMPT.md"), prompt);
@@ -835,15 +835,15 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
? task.dependencies.map((d) => `- **Task:** ${d}`).join("\n")
: "- **None**";
const heading = task.title || task.description;
return `# ${task.id}: ${heading}
const heading = task.title ? `${task.id}: ${task.title}` : task.id;
return `# ${heading}
**Created:** ${task.createdAt.split("T")[0]}
**Size:** M
## Mission
${task.description || task.title}
${task.description}
## Dependencies

View File

@@ -154,7 +154,7 @@ export function TaskDetailModal({
{task.prompt ? (
<div className="markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{task.prompt}
{task.prompt.replace(/^#\s+[^\n]*\n+/, "")}
</ReactMarkdown>
</div>
) : (

View File

@@ -41,7 +41,7 @@ describe("TaskDetailModal", () => {
expect(markdownDiv!.classList.contains("detail-prompt")).toBe(false);
});
it("renders ReactMarkdown output (heading and bold text)", () => {
it("strips the leading heading from prompt and renders remaining markdown", () => {
const { container } = render(
<TaskDetailModal
task={makeTask({ prompt: "# Hello\n\nSome **bold** text" })}
@@ -53,7 +53,8 @@ describe("TaskDetailModal", () => {
/>,
);
expect(container.querySelector("h1")?.textContent).toBe("Hello");
// The leading # heading should be stripped (modal has its own header)
expect(container.querySelector(".markdown-body h1")).toBeNull();
expect(container.querySelector("strong")?.textContent).toBe("bold");
});
@@ -89,4 +90,29 @@ describe("TaskDetailModal", () => {
expect(screen.queryByText("PROMPT.md")).toBeNull();
});
it("shows description exactly once for a task without title", () => {
const { container } = render(
<TaskDetailModal
task={makeTask({
title: undefined,
description: "Fix the login bug",
prompt: "# HAI-099\n\nFix the login bug\n",
})}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
addToast={noop}
/>,
);
// The heading "HAI-099" should be stripped from the markdown
const markdownBody = container.querySelector(".markdown-body");
expect(markdownBody?.innerHTML).not.toContain("HAI-099");
// Description appears in the markdown body
expect(markdownBody?.textContent).toContain("Fix the login bug");
// The detail header shows the ID (not duplicated as markdown heading)
expect(container.querySelector(".detail-id")?.textContent).toBe("HAI-099");
});
});