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:
gsxdsm
2026-04-02 19:17:45 -07:00
parent 51a4efa70c
commit 302d6dc634
2 changed files with 15 additions and 133 deletions

View File

@@ -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)}

View File

@@ -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);
});
});
});