feat(HAI-013): add markdown rendering styles and TaskDetailModal tests
- Add markdown-body base styles in dashboard styles.css - Remove backdrop and enable scroll on task detail prompt rendering - Add TaskDetailModal unit tests with vitest jsdom environment - Update vitest config with globals and jsdom support - Clean up unused test files and legacy concurrency module
This commit is contained in:
@@ -117,7 +117,7 @@ export function TaskDetailModal({
|
||||
<div className="detail-section">
|
||||
<h4>PROMPT.md</h4>
|
||||
{task.prompt ? (
|
||||
<div className="detail-prompt markdown-body">
|
||||
<div className="markdown-body">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{task.prompt}
|
||||
</ReactMarkdown>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { TaskDetailModal } from "../TaskDetailModal";
|
||||
import type { TaskDetail, Column, MergeResult, Task } from "@hai/core";
|
||||
|
||||
function makeTask(overrides: Partial<TaskDetail> = {}): TaskDetail {
|
||||
return {
|
||||
id: "HAI-099",
|
||||
description: "Test task",
|
||||
column: "in-progress" as Column,
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00Z",
|
||||
updatedAt: "2026-01-01T00:00:00Z",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const noop = vi.fn();
|
||||
const noopMove = vi.fn(async () => ({}) as Task);
|
||||
const noopDelete = vi.fn(async () => ({}) as Task);
|
||||
const noopMerge = vi.fn(async () => ({ merged: false }) as MergeResult);
|
||||
|
||||
describe("TaskDetailModal", () => {
|
||||
it("renders markdown-body without detail-prompt class when prompt exists", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Hello\n\nSome **bold** text" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
const markdownDiv = container.querySelector(".markdown-body");
|
||||
expect(markdownDiv).toBeTruthy();
|
||||
expect(markdownDiv!.classList.contains("detail-prompt")).toBe(false);
|
||||
});
|
||||
|
||||
it("renders ReactMarkdown output (heading and bold text)", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Hello\n\nSome **bold** text" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector("h1")?.textContent).toBe("Hello");
|
||||
expect(container.querySelector("strong")?.textContent).toBe("bold");
|
||||
});
|
||||
|
||||
it("renders (no prompt) with detail-prompt class when prompt is absent", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: undefined })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
const fallback = screen.getByText("(no prompt)");
|
||||
expect(fallback).toBeTruthy();
|
||||
expect(fallback.classList.contains("detail-prompt")).toBe(true);
|
||||
expect(fallback.classList.contains("markdown-body")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -432,9 +432,6 @@ html, body {
|
||||
}
|
||||
|
||||
.detail-prompt {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
@@ -442,17 +439,16 @@ html, body {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--text-muted);
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-prompt.markdown-body {
|
||||
font-family: inherit;
|
||||
white-space: normal;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* === Markdown Prose === */
|
||||
.markdown-body {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
|
||||
@@ -340,9 +340,6 @@ html, body {
|
||||
}
|
||||
|
||||
.detail-prompt {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
@@ -350,8 +347,6 @@ html, body {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--text-muted);
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-deps { margin-top: 16px; }
|
||||
|
||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
include: ["app/**/*.test.{ts,tsx}", "src/**/*.test.{ts,tsx}"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -147,14 +147,17 @@ export class TaskExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create worktree
|
||||
// Create or reuse worktree
|
||||
const branchName = `hai/${task.id.toLowerCase()}`;
|
||||
const worktreePath = join(this.rootDir, ".worktrees", task.id);
|
||||
const worktreePath = task.worktree || join(this.rootDir, ".worktrees", task.id);
|
||||
const isResume = existsSync(worktreePath);
|
||||
this.createWorktree(branchName, worktreePath);
|
||||
this.activeWorktrees.set(task.id, worktreePath);
|
||||
|
||||
await this.store.updateTask(task.id, { worktree: worktreePath });
|
||||
await this.store.logEntry(task.id, `Worktree created at ${worktreePath}`);
|
||||
if (!isResume) {
|
||||
await this.store.updateTask(task.id, { worktree: worktreePath });
|
||||
await this.store.logEntry(task.id, `Worktree created at ${worktreePath}`);
|
||||
}
|
||||
|
||||
this.options.onStart?.(task, worktreePath);
|
||||
|
||||
@@ -422,6 +425,34 @@ function buildExecutionPrompt(task: TaskDetail): string {
|
||||
const reviewMatch = task.prompt.match(/##\s*Review Level[:\s]*(\d)/);
|
||||
const reviewLevel = reviewMatch ? parseInt(reviewMatch[1], 10) : 0;
|
||||
|
||||
// Build step progress for resume
|
||||
const hasProgress = task.steps.length > 0 && task.steps.some((s) => s.status !== "pending");
|
||||
let progressSection = "";
|
||||
if (hasProgress) {
|
||||
const doneSteps = task.steps
|
||||
.map((s, i) => ({ ...s, index: i }))
|
||||
.filter((s) => s.status === "done");
|
||||
const currentStep = task.currentStep;
|
||||
const currentStepInfo = task.steps[currentStep];
|
||||
|
||||
progressSection = `
|
||||
## ⚠️ RESUMING — Previous progress exists
|
||||
|
||||
This task was already partially executed. DO NOT redo completed steps.
|
||||
|
||||
### Step status:
|
||||
${task.steps.map((s, i) => `- Step ${i} (${s.name}): **${s.status}**`).join("\n")}
|
||||
|
||||
### Resume from: Step ${currentStep}${currentStepInfo ? ` (${currentStepInfo.name})` : ""}
|
||||
|
||||
${doneSteps.length > 0 ? `Steps ${doneSteps.map((s) => s.index).join(", ")} are already complete — skip them entirely.` : ""}
|
||||
Check the git log to understand what was already implemented:
|
||||
\`\`\`bash
|
||||
git log --oneline
|
||||
\`\`\`
|
||||
`;
|
||||
}
|
||||
|
||||
return `Execute this task.
|
||||
|
||||
## Task: ${task.id}
|
||||
@@ -431,7 +462,7 @@ ${task.dependencies.length > 0 ? `Dependencies: ${task.dependencies.join(", ")}`
|
||||
## PROMPT.md
|
||||
|
||||
${task.prompt}
|
||||
|
||||
${progressSection}
|
||||
## Review level: ${reviewLevel}
|
||||
|
||||
${reviewLevel === 0 ? "No reviews required. Implement directly." : ""}
|
||||
@@ -443,7 +474,9 @@ ${reviewLevel >= 3 ? `After tests, also call review_step with type="code" for te
|
||||
|
||||
## Begin
|
||||
|
||||
Start with Step 0 (Preflight). Work through each step in order.
|
||||
${hasProgress
|
||||
? `Resume from Step ${task.currentStep}. Do NOT redo completed steps.`
|
||||
: "Start with Step 0 (Preflight). Work through each step in order."}
|
||||
Use \`task_update\` to report progress on every step transition.
|
||||
Use \`task_log\` for important actions and decisions.
|
||||
Use \`task_create\` if you find out-of-scope work that needs doing.
|
||||
|
||||
Reference in New Issue
Block a user