feat: @project mentions with colored chips in markdown and editors

Add project mention system using project:// URI scheme with optional
color parameter. Mentions render as colored pill chips in markdown
bodies and the WYSIWYG editor. Autocomplete in editors shows both
agents and projects. Server extracts mentioned project IDs from issue
content and returns them in the issue detail response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dotta
2026-03-02 13:31:58 -06:00
parent 19e2cf3793
commit 2488dc703c
13 changed files with 397 additions and 13 deletions

View File

@@ -15,6 +15,7 @@ import {
projectWorkspaces,
projects,
} from "@paperclip/db";
import { extractProjectMentionIds } from "@paperclip/shared";
import { conflict, notFound, unprocessable } from "../errors.js";
const ALL_ISSUE_STATUSES = ["backlog", "todo", "in_progress", "in_review", "blocked", "done", "cancelled"];
@@ -907,6 +908,48 @@ export function issueService(db: Db) {
return rows.filter(a => tokens.has(a.name.toLowerCase())).map(a => a.id);
},
findMentionedProjectIds: async (issueId: string) => {
const issue = await db
.select({
companyId: issues.companyId,
title: issues.title,
description: issues.description,
})
.from(issues)
.where(eq(issues.id, issueId))
.then((rows) => rows[0] ?? null);
if (!issue) return [];
const comments = await db
.select({ body: issueComments.body })
.from(issueComments)
.where(eq(issueComments.issueId, issueId));
const mentionedIds = new Set<string>();
for (const source of [
issue.title,
issue.description ?? "",
...comments.map((comment) => comment.body),
]) {
for (const projectId of extractProjectMentionIds(source)) {
mentionedIds.add(projectId);
}
}
if (mentionedIds.size === 0) return [];
const rows = await db
.select({ id: projects.id })
.from(projects)
.where(
and(
eq(projects.companyId, issue.companyId),
inArray(projects.id, [...mentionedIds]),
),
);
const valid = new Set(rows.map((row) => row.id));
return [...mentionedIds].filter((projectId) => valid.has(projectId));
},
getAncestors: async (issueId: string) => {
const raw: Array<{
id: string; identifier: string | null; title: string; description: string | null;