Files
fusion/packages/dashboard/app/components/SteeringTab.tsx
gsxdsm afc24408cc feat(KB-622): unify steeringComments and comments into single comments field
- Merge steeringComments and comments into unified comments field in Task type
- Update TaskStore to use single comments array instead of separate steeringComments
- Add database migration to convert existing steeringComments to comments
- Update executor to inject all comments into AI execution context
- Update dashboard SteeringTab to use unified comments API
- Update CLI task steer command to use comments field
- Update PR comment handler to add comments via unified API
2026-04-01 07:05:23 -07:00

170 lines
5.4 KiB
TypeScript

import { useState, useCallback } from "react";
import type { TaskDetail } from "@fusion/core";
import { addComment } from "../api";
import type { ToastType } from "../hooks/useToast";
function formatTimestamp(iso: string): string {
const date = new Date(iso);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMin = Math.floor(diffMs / 60000);
const diffHr = Math.floor(diffMin / 60);
const diffDay = Math.floor(diffHr / 24);
if (diffMin < 1) return "just now";
if (diffMin < 60) return `${diffMin}m ago`;
if (diffHr < 24) return `${diffHr}h ago`;
if (diffDay < 7) return `${diffDay}d ago`;
return date.toLocaleDateString();
}
interface SteeringTabProps {
task: TaskDetail;
addToast: (message: string, type?: ToastType) => void;
}
export function SteeringTab({ task, addToast }: SteeringTabProps) {
const [comments, setComments] = useState(task.comments || []);
const [newComment, setNewComment] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const MAX_LENGTH = 2000;
const handleSubmit = useCallback(async () => {
if (!newComment.trim() || newComment.length > MAX_LENGTH || isSubmitting) return;
setIsSubmitting(true);
try {
const updated = await addComment(task.id, newComment.trim());
setComments(updated.comments || []);
setNewComment("");
addToast("Comment added", "success");
} catch (err: any) {
addToast(err.message, "error");
} finally {
setIsSubmitting(false);
}
}, [task.id, newComment, isSubmitting, addToast]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
handleSubmit();
}
},
[handleSubmit]
);
const isValid = newComment.trim().length > 0 && newComment.length <= MAX_LENGTH;
return (
<div className="detail-section">
<h4>Comments</h4>
<p style={{ fontSize: "13px", opacity: 0.7, marginBottom: "12px" }}>
Add comments to guide the AI during task execution. These are injected into the execution context.
</p>
{comments.length > 0 ? (
<div className="detail-activity-list" style={{ marginBottom: "16px" }}>
{[...comments].reverse().map((comment) => (
<div key={comment.id} className="detail-log-entry">
<div className="detail-log-header">
<span
className="detail-log-timestamp"
style={{
display: "inline-flex",
alignItems: "center",
gap: "6px",
}}
>
<span
style={{
fontSize: "11px",
padding: "2px 6px",
borderRadius: "4px",
background:
comment.author === "user"
? "var(--accent-primary, #6366f1)"
: "var(--accent-secondary, #8b5cf6)",
color: "#fff",
}}
>
{comment.author}
</span>
{formatTimestamp(comment.createdAt)}
</span>
</div>
<div
style={{
marginTop: "4px",
padding: "8px 12px",
background: "var(--bg-secondary)",
borderRadius: "6px",
border: "1px solid var(--border, #333)",
fontSize: "14px",
lineHeight: "1.5",
whiteSpace: "pre-wrap",
}}
>
{comment.text}
</div>
</div>
))}
</div>
) : (
<div style={{ opacity: 0.5, marginBottom: "16px" }}>(no comments yet)</div>
)}
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Add a comment... (Ctrl+Enter to submit)"
maxLength={MAX_LENGTH}
rows={4}
style={{
width: "100%",
padding: "12px",
fontSize: "14px",
fontFamily: "inherit",
background: "var(--bg-secondary)",
border: "1px solid var(--border, #333)",
borderRadius: "6px",
color: "inherit",
resize: "vertical",
}}
/>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span
style={{
fontSize: "12px",
opacity: newComment.length > MAX_LENGTH ? 0.9 : 0.5,
color:
newComment.length > MAX_LENGTH
? "var(--error, #ef4444)"
: "inherit",
}}
>
{newComment.length} / {MAX_LENGTH}
</span>
<button
className="btn btn-sm btn-primary"
onClick={handleSubmit}
disabled={!isValid || isSubmitting}
>
{isSubmitting ? "Adding…" : "Add Comment"}
</button>
</div>
</div>
</div>
);
}