feat: use markdown editor with @-mentions for issue comments

- Replaced plain textarea in CommentThread with MarkdownEditor
  for rich markdown editing (no toolbar, compact styling)
- Added @-mention autocomplete to MarkdownEditor:
  - Detects @ trigger while typing with cursor-positioned dropdown
  - Arrow key navigation, Enter/Tab to select, Escape to dismiss
  - Filters mentionable names as user types after @
- Added onSubmit prop to MarkdownEditor for Cmd/Ctrl+Enter support
- Agents from the company are passed as mentionable options

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-20 13:35:15 -06:00
parent c50223f68f
commit 0cf33695d3
2 changed files with 227 additions and 20 deletions

View File

@@ -1,10 +1,10 @@
import { useMemo, useState } from "react";
import { useMemo, useRef, useState } from "react";
import { Link } from "react-router-dom";
import Markdown from "react-markdown";
import type { IssueComment, Agent } from "@paperclip/shared";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Identity } from "./Identity";
import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor";
import { formatDate } from "../lib/utils";
interface CommentWithRunMeta extends IssueComment {
@@ -25,6 +25,7 @@ export function CommentThread({ comments, onAdd, issueStatus, agentMap }: Commen
const [body, setBody] = useState("");
const [reopen, setReopen] = useState(true);
const [submitting, setSubmitting] = useState(false);
const editorRef = useRef<MarkdownEditorRef>(null);
const isClosed = issueStatus ? CLOSED_STATUSES.has(issueStatus) : false;
@@ -34,8 +35,16 @@ export function CommentThread({ comments, onAdd, issueStatus, agentMap }: Commen
[comments],
);
async function handleSubmit(e?: React.FormEvent) {
e?.preventDefault();
// Build mention options from agent map
const mentions = useMemo<MentionOption[]>(() => {
if (!agentMap) return [];
return Array.from(agentMap.values()).map((a) => ({
id: a.id,
name: a.name,
}));
}, [agentMap]);
async function handleSubmit() {
const trimmed = body.trim();
if (!trimmed) return;
@@ -90,18 +99,15 @@ export function CommentThread({ comments, onAdd, issueStatus, agentMap }: Commen
))}
</div>
<form onSubmit={handleSubmit} className="space-y-2">
<Textarea
placeholder="Leave a comment..."
<div className="space-y-2">
<MarkdownEditor
ref={editorRef}
value={body}
onChange={(e) => setBody(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
handleSubmit();
}
}}
rows={3}
onChange={setBody}
placeholder="Leave a comment..."
mentions={mentions}
onSubmit={handleSubmit}
contentClassName="min-h-[60px] text-sm"
/>
<div className="flex items-center justify-end gap-3">
{isClosed && (
@@ -115,11 +121,11 @@ export function CommentThread({ comments, onAdd, issueStatus, agentMap }: Commen
Re-open
</label>
)}
<Button type="submit" size="sm" disabled={!body.trim() || submitting}>
<Button size="sm" disabled={!body.trim() || submitting} onClick={handleSubmit}>
{submitting ? "Posting..." : "Comment"}
</Button>
</div>
</form>
</div>
</div>
);
}