feat(FN-735): remove comment mode toggle and unify to steering comments
- Remove useSteeringComments toggle from SettingsModal and task comment UI - Unify TaskComments to always use the steering comment path - Add Help button in Header linking to documentation - Update SettingsModal and TaskComments tests for unified comment behavior - Clean up obsolete dual-mode comment tests
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import type { Task, TaskComment } from "@fusion/core";
|
||||
import { addSteeringComment, addTaskComment, updateTaskComment, deleteTaskComment } from "../api";
|
||||
import { addSteeringComment, updateTaskComment, deleteTaskComment } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
const MAX_COMMENT_LENGTH = 2000;
|
||||
@@ -13,8 +13,6 @@ interface TaskCommentsProps {
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
type CommentType = "comment" | "guidance";
|
||||
|
||||
function formatCommentTimestamp(comment: TaskComment): string {
|
||||
const timestamp = comment.updatedAt || comment.createdAt;
|
||||
const label = new Date(timestamp).toLocaleString();
|
||||
@@ -31,8 +29,6 @@ export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "u
|
||||
const [editingText, setEditingText] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
const [commentType, setCommentType] = useState<CommentType>("comment");
|
||||
|
||||
// Sort comments by createdAt descending (newest first)
|
||||
const comments = useMemo(() => {
|
||||
return [...(task.comments || [])].sort(
|
||||
@@ -47,17 +43,10 @@ export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "u
|
||||
if (!text) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
if (commentType === "guidance") {
|
||||
const updated = await addSteeringComment(task.id, text, projectId);
|
||||
setDraft("");
|
||||
onTaskUpdated?.(updated);
|
||||
addToast("AI Guidance added", "success");
|
||||
} else {
|
||||
const updated = await addTaskComment(task.id, text, currentAuthor, projectId);
|
||||
setDraft("");
|
||||
onTaskUpdated?.(updated);
|
||||
addToast("Comment added", "success");
|
||||
}
|
||||
const updated = await addSteeringComment(task.id, text, projectId);
|
||||
setDraft("");
|
||||
onTaskUpdated?.(updated);
|
||||
addToast("Comment added", "success");
|
||||
} catch (error: any) {
|
||||
addToast(error.message || "Failed to add comment", "error");
|
||||
} finally {
|
||||
@@ -102,8 +91,8 @@ export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "u
|
||||
}
|
||||
}
|
||||
|
||||
const placeholder = commentType === "guidance" ? "Add guidance for the AI agent" : "Add a comment";
|
||||
const buttonLabel = commentType === "guidance" ? "Add Guidance" : "Add Comment";
|
||||
const placeholder = "Add a comment";
|
||||
const buttonLabel = "Add Comment";
|
||||
|
||||
return (
|
||||
<div className="detail-section">
|
||||
@@ -187,25 +176,6 @@ export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "u
|
||||
)}
|
||||
|
||||
<div className="comments-compose-form">
|
||||
<div className="comments-type-toggle">
|
||||
<button
|
||||
className={`btn btn-sm ${commentType === "comment" ? "btn-primary" : ""}`}
|
||||
onClick={() => setCommentType("comment")}
|
||||
>
|
||||
Comment
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-sm ${commentType === "guidance" ? "btn-primary" : ""}`}
|
||||
onClick={() => setCommentType("guidance")}
|
||||
>
|
||||
AI Guidance
|
||||
</button>
|
||||
</div>
|
||||
{commentType === "guidance" && (
|
||||
<div className="comments-guidance-hint">
|
||||
AI Guidance comments are injected into the task execution context
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
|
||||
@@ -4,12 +4,11 @@ import { TaskComments } from "../TaskComments";
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
addSteeringComment: vi.fn(),
|
||||
addTaskComment: vi.fn(),
|
||||
updateTaskComment: vi.fn(),
|
||||
deleteTaskComment: vi.fn(),
|
||||
}));
|
||||
|
||||
import { addSteeringComment, addTaskComment, updateTaskComment, deleteTaskComment } from "../../api";
|
||||
import { addSteeringComment, updateTaskComment, deleteTaskComment } from "../../api";
|
||||
|
||||
const makeTask = (overrides: any = {}) => ({
|
||||
id: "FN-001",
|
||||
@@ -34,15 +33,15 @@ describe("TaskComments", () => {
|
||||
expect(screen.getByText("No comments yet.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("adds a user comment via addTaskComment API", async () => {
|
||||
it("adds a comment via addSteeringComment API", async () => {
|
||||
const onTaskUpdated = vi.fn();
|
||||
vi.mocked(addTaskComment).mockResolvedValue(makeTask({ comments: [{ id: "c1", text: "Hello", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }] }));
|
||||
vi.mocked(addSteeringComment).mockResolvedValue(makeTask({ comments: [{ id: "c1", text: "Hello", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }] }));
|
||||
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} onTaskUpdated={onTaskUpdated} />);
|
||||
fireEvent.change(screen.getByPlaceholderText(/Add a comment/), { target: { value: "Hello" } });
|
||||
fireEvent.click(screen.getByText("Add Comment"));
|
||||
|
||||
await waitFor(() => expect(addTaskComment).toHaveBeenCalledWith("FN-001", "Hello", "user", undefined));
|
||||
await waitFor(() => expect(addSteeringComment).toHaveBeenCalledWith("FN-001", "Hello", undefined));
|
||||
expect(onTaskUpdated).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -131,93 +130,6 @@ describe("TaskComments", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("comment type selector", () => {
|
||||
it("shows Comment and AI Guidance type selector buttons", () => {
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.getByText("Comment")).toBeTruthy();
|
||||
expect(screen.getByText("AI Guidance")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("defaults to Comment type", () => {
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
const commentBtn = screen.getByText("Comment");
|
||||
expect(commentBtn.className).toContain("btn-primary");
|
||||
});
|
||||
|
||||
it("shows helper text when AI Guidance type is selected", () => {
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
// Click AI Guidance button
|
||||
fireEvent.click(screen.getByText("AI Guidance"));
|
||||
|
||||
expect(screen.getByText(/AI Guidance comments are injected into the task execution context/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("uses addSteeringComment API when AI Guidance type is selected", async () => {
|
||||
vi.mocked(addSteeringComment).mockResolvedValue(makeTask({
|
||||
comments: [{ id: "c1", text: "Guidance text", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }],
|
||||
}));
|
||||
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
// Select AI Guidance type
|
||||
fireEvent.click(screen.getByText("AI Guidance"));
|
||||
|
||||
// Enter text and submit
|
||||
fireEvent.change(screen.getByPlaceholderText(/Add guidance/), { target: { value: "Guidance text" } });
|
||||
fireEvent.click(screen.getByText("Add Guidance"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addSteeringComment).toHaveBeenCalledWith("FN-001", "Guidance text", undefined);
|
||||
expect(addTaskComment).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("uses addTaskComment API when Comment type is selected", async () => {
|
||||
vi.mocked(addTaskComment).mockResolvedValue(makeTask({
|
||||
comments: [{ id: "c1", text: "User text", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }],
|
||||
}));
|
||||
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
// Comment type is default
|
||||
fireEvent.change(screen.getByPlaceholderText(/Add a comment/), { target: { value: "User text" } });
|
||||
fireEvent.click(screen.getByText("Add Comment"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addTaskComment).toHaveBeenCalledWith("FN-001", "User text", "user", undefined);
|
||||
expect(addSteeringComment).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("changes placeholder text based on selected type", () => {
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
// Default: Comment type
|
||||
expect(screen.getByPlaceholderText(/Add a comment/)).toBeTruthy();
|
||||
|
||||
// Switch to AI Guidance
|
||||
fireEvent.click(screen.getByText("AI Guidance"));
|
||||
expect(screen.getByPlaceholderText(/Add guidance for the AI agent/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("changes submit button label based on selected type", () => {
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
// Default: "Add Comment" button
|
||||
expect(screen.getByText("Add Comment")).toBeTruthy();
|
||||
|
||||
// Enable submit
|
||||
fireEvent.change(screen.getByPlaceholderText(/Add a comment/), { target: { value: "text" } });
|
||||
|
||||
// Switch to AI Guidance
|
||||
fireEvent.click(screen.getByText("AI Guidance"));
|
||||
expect(screen.getByText("Add Guidance")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("character count", () => {
|
||||
it("shows character count", () => {
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
@@ -241,7 +153,7 @@ describe("TaskComments", () => {
|
||||
|
||||
describe("keyboard shortcuts", () => {
|
||||
it("submits comment on Ctrl+Enter", async () => {
|
||||
vi.mocked(addTaskComment).mockResolvedValue(makeTask({
|
||||
vi.mocked(addSteeringComment).mockResolvedValue(makeTask({
|
||||
comments: [{ id: "c1", text: "Keyboard", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }],
|
||||
}));
|
||||
|
||||
@@ -252,12 +164,12 @@ describe("TaskComments", () => {
|
||||
fireEvent.keyDown(textarea, { key: "Enter", ctrlKey: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addTaskComment).toHaveBeenCalledWith("FN-001", "Keyboard", "user", undefined);
|
||||
expect(addSteeringComment).toHaveBeenCalledWith("FN-001", "Keyboard", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("submits comment on Cmd+Enter", async () => {
|
||||
vi.mocked(addTaskComment).mockResolvedValue(makeTask({
|
||||
vi.mocked(addSteeringComment).mockResolvedValue(makeTask({
|
||||
comments: [{ id: "c1", text: "Mac", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }],
|
||||
}));
|
||||
|
||||
@@ -268,7 +180,7 @@ describe("TaskComments", () => {
|
||||
fireEvent.keyDown(textarea, { key: "Enter", metaKey: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addTaskComment).toHaveBeenCalledWith("FN-001", "Mac", "user", undefined);
|
||||
expect(addSteeringComment).toHaveBeenCalledWith("FN-001", "Mac", undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user