mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
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:
parent
19e2cf3793
commit
2488dc703c
13 changed files with 397 additions and 13 deletions
|
|
@ -228,6 +228,13 @@ export {
|
|||
|
||||
export { API_PREFIX, API } from "./api.js";
|
||||
export { normalizeAgentUrlKey, deriveAgentUrlKey } from "./agent-url-key.js";
|
||||
export {
|
||||
PROJECT_MENTION_SCHEME,
|
||||
buildProjectMentionHref,
|
||||
parseProjectMentionHref,
|
||||
extractProjectMentionIds,
|
||||
type ParsedProjectMention,
|
||||
} from "./project-mentions.js";
|
||||
|
||||
export {
|
||||
paperclipConfigSchema,
|
||||
|
|
|
|||
78
packages/shared/src/project-mentions.ts
Normal file
78
packages/shared/src/project-mentions.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
export const PROJECT_MENTION_SCHEME = "project://";
|
||||
|
||||
const HEX_COLOR_RE = /^[0-9a-f]{6}$/i;
|
||||
const HEX_COLOR_SHORT_RE = /^[0-9a-f]{3}$/i;
|
||||
const HEX_COLOR_WITH_HASH_RE = /^#[0-9a-f]{6}$/i;
|
||||
const HEX_COLOR_SHORT_WITH_HASH_RE = /^#[0-9a-f]{3}$/i;
|
||||
const PROJECT_MENTION_LINK_RE = /\[[^\]]*]\((project:\/\/[^)\s]+)\)/gi;
|
||||
|
||||
export interface ParsedProjectMention {
|
||||
projectId: string;
|
||||
color: string | null;
|
||||
}
|
||||
|
||||
function normalizeHexColor(input: string | null | undefined): string | null {
|
||||
if (!input) return null;
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
if (HEX_COLOR_WITH_HASH_RE.test(trimmed)) {
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
if (HEX_COLOR_RE.test(trimmed)) {
|
||||
return `#${trimmed.toLowerCase()}`;
|
||||
}
|
||||
if (HEX_COLOR_SHORT_WITH_HASH_RE.test(trimmed)) {
|
||||
const raw = trimmed.slice(1).toLowerCase();
|
||||
return `#${raw[0]}${raw[0]}${raw[1]}${raw[1]}${raw[2]}${raw[2]}`;
|
||||
}
|
||||
if (HEX_COLOR_SHORT_RE.test(trimmed)) {
|
||||
const raw = trimmed.toLowerCase();
|
||||
return `#${raw[0]}${raw[0]}${raw[1]}${raw[1]}${raw[2]}${raw[2]}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildProjectMentionHref(projectId: string, color?: string | null): string {
|
||||
const trimmedProjectId = projectId.trim();
|
||||
const normalizedColor = normalizeHexColor(color ?? null);
|
||||
if (!normalizedColor) {
|
||||
return `${PROJECT_MENTION_SCHEME}${trimmedProjectId}`;
|
||||
}
|
||||
return `${PROJECT_MENTION_SCHEME}${trimmedProjectId}?c=${encodeURIComponent(normalizedColor.slice(1))}`;
|
||||
}
|
||||
|
||||
export function parseProjectMentionHref(href: string): ParsedProjectMention | null {
|
||||
if (!href.startsWith(PROJECT_MENTION_SCHEME)) return null;
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(href);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (url.protocol !== "project:") return null;
|
||||
|
||||
const projectId = `${url.hostname}${url.pathname}`.replace(/^\/+/, "").trim();
|
||||
if (!projectId) return null;
|
||||
|
||||
const color = normalizeHexColor(url.searchParams.get("c") ?? url.searchParams.get("color"));
|
||||
|
||||
return {
|
||||
projectId,
|
||||
color,
|
||||
};
|
||||
}
|
||||
|
||||
export function extractProjectMentionIds(markdown: string): string[] {
|
||||
if (!markdown) return [];
|
||||
const ids = new Set<string>();
|
||||
const re = new RegExp(PROJECT_MENTION_LINK_RE);
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = re.exec(markdown)) !== null) {
|
||||
const parsed = parseProjectMentionHref(match[1]);
|
||||
if (parsed) ids.add(parsed.projectId);
|
||||
}
|
||||
return [...ids];
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import type { IssuePriority, IssueStatus } from "../constants.js";
|
||||
import type { ProjectWorkspace } from "./project.js";
|
||||
import type { Goal } from "./goal.js";
|
||||
import type { Project, ProjectWorkspace } from "./project.js";
|
||||
|
||||
export interface IssueAncestorProject {
|
||||
id: string;
|
||||
|
|
@ -78,6 +79,9 @@ export interface Issue {
|
|||
hiddenAt: Date | null;
|
||||
labelIds?: string[];
|
||||
labels?: IssueLabel[];
|
||||
project?: Project | null;
|
||||
goal?: Goal | null;
|
||||
mentionedProjects?: Project[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue