chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,366 @@
/**
* Block Kit parity tests for the JSX render components. Each component is a
* `@copilotkit/channels-ui` `ComponentFn`; we assert the full
* `renderSlackMessage(renderToIR(<… />))` output — both the `blocks` and the
* attachment `accent` — against the legacy `defineSlackComponent` shapes.
*
* The shared IR→mrkdwn path runs section/field/context text through
* `markdownToMrkdwn`, so the components author Markdown bold (`**x**`) which
* the transform rewrites into Slack bold (`*x*`). The block structure,
* ordering, emoji, dividers, footers and accent colors match the legacy
* `.ts` output, and the link/label forms below assert the Slack-bold `*…*`
* the old `defineSlackComponent` code produced.
*
* Status/priority glyphs are now platform-neutral unicode (✅ 🔵 🚨 🔴 etc.)
* so they render identically on both Slack and Telegram.
*/
import { describe, it, expect } from "vitest";
import { renderToIR } from "@copilotkit/channels-ui";
import { renderSlackMessage } from "@copilotkit/channels-slack";
import { renderTelegram } from "@copilotkit/channels-telegram";
import { IssueList } from "../issue-list.js";
import { IssueCard } from "../issue-card.js";
import { PageList } from "../page-list.js";
describe("IssueList component", () => {
it("renders exactly three blocks: header, a single section with one line per issue, and a count footer", () => {
const { blocks, accent } = renderSlackMessage(
renderToIR(
<IssueList
heading="Open"
issues={[
{
identifier: "CPK-101",
title: "Checkout 500s under load",
url: "https://linear.app/copilotkit/issue/CPK-101",
state: "In Progress",
assignee: "Alem",
priority: "Urgent",
updated: "2d ago",
},
{
identifier: "CPK-102",
title: "Login redirect loop",
url: "https://linear.app/copilotkit/issue/CPK-102",
state: "Todo",
assignee: "Sam",
priority: "High",
updated: "5h ago",
},
]}
/>,
),
);
// Fixed three-block layout regardless of issue count.
expect(blocks).toHaveLength(3);
expect(blocks[0]).toMatchObject({
type: "header",
text: { type: "plain_text", text: "📋 Open" },
});
expect(blocks[1]).toMatchObject({ type: "section" });
expect(blocks[2]).toMatchObject({ type: "context" });
const section = blocks[1] as { text: { type: string; text: string } };
expect(section.text.type).toBe("mrkdwn");
const text = section.text.text;
// One line per issue, joined by newlines.
expect(text.split("\n")).toHaveLength(2);
// Each issue is a linked, bold identifier (Markdown bold → Slack bold).
expect(text).toContain(
"<https://linear.app/copilotkit/issue/CPK-101|*CPK-101*>",
);
expect(text).toContain(
"<https://linear.app/copilotkit/issue/CPK-102|*CPK-102*>",
);
// Titles, assignees and updated meta are inline on the line.
expect(text).toContain("Checkout 500s under load");
expect(text).toContain("Login redirect loop");
expect(text).toContain("Alem");
expect(text).toContain("Sam");
expect(text).toContain("2d ago");
// In-progress maps to the blue dot.
expect(text).toContain("🔵");
// Count footer.
expect(JSON.stringify(blocks[2])).toContain("2 issues");
// Hottest priority (Urgent) drives the accent.
expect(accent).toBe("#EB5757");
});
it("caps the section at 15 lines and reports the overflow in the footer", () => {
const issues = Array.from({ length: 20 }, (_, i) => ({
identifier: `CPK-${i + 1}`,
title: `Issue ${i + 1}`,
}));
const { blocks } = renderSlackMessage(
renderToIR(<IssueList heading="Many" issues={issues} />),
);
expect(blocks).toHaveLength(3);
const section = blocks[1] as { text: { text: string } };
// Only the first 15 issues are rendered.
expect(section.text.text.split("\n")).toHaveLength(15);
expect(section.text.text).toContain("*CPK-1*");
expect(section.text.text).toContain("*CPK-15*");
expect(section.text.text).not.toContain("*CPK-16*");
// Footer surfaces the overflow.
expect(JSON.stringify(blocks[2])).toContain("Showing 15 of 20 issues");
});
it("falls back to an emphasized identifier and 'unassigned' when fields are missing", () => {
const { blocks, accent } = renderSlackMessage(
renderToIR(
<IssueList issues={[{ identifier: "CPK-9", title: "No assignee" }]} />,
),
);
const json = JSON.stringify(blocks);
// No url → bold identifier, no link wrapper.
expect(json).toContain("*CPK-9*");
expect(json).not.toContain("|*CPK-9*>");
expect(json).toContain("unassigned");
// No urgent/high priority → Linear purple.
expect(accent).toBe("#5E6AD2");
});
});
describe("IssueCard component", () => {
it("renders a status header, linked title and a fields grid", () => {
const { blocks, accent } = renderSlackMessage(
renderToIR(
<IssueCard
identifier="CPK-101"
title="Checkout 500s under load"
url="https://linear.app/copilotkit/issue/CPK-101"
state="In Progress"
assignee="Alem"
priority="Urgent"
team="CPK"
/>,
),
);
const json = JSON.stringify(blocks);
// Header: in-progress unicode dot + identifier (plain_text, untouched).
expect(blocks[0]).toMatchObject({
type: "header",
text: { type: "plain_text", text: "🔵 CPK-101" },
});
// Title section with the linked, bold title.
expect(json).toContain(
"<https://linear.app/copilotkit/issue/CPK-101|*Checkout 500s under load*>",
);
// A section carries the 2-column metadata grid.
const fieldsSection = blocks.find(
(b) => b.type === "section" && "fields" in b && Array.isArray(b.fields),
) as { fields: { text: string }[] } | undefined;
expect(fieldsSection).toBeDefined();
expect(fieldsSection?.fields).toHaveLength(4);
expect(json).toContain("*Assignee*\\nAlem");
expect(json).toContain("*Priority*\\n🚨 Urgent");
expect(json).toContain("*Status*\\n🔵 In Progress");
expect(json).toContain("*Team*\\nCPK");
// Footer: "Open in Linear" link.
expect(json).toContain(
"<https://linear.app/copilotkit/issue/CPK-101|Open in Linear →>",
);
// Urgent priority drives the accent.
expect(accent).toBe("#EB5757");
});
it("shows a 'Filed' banner and a check header when justCreated", () => {
const { blocks, accent } = renderSlackMessage(
renderToIR(
<IssueCard identifier="CPK-200" title="New bug" justCreated />,
),
);
const json = JSON.stringify(blocks);
expect(blocks[0]).toMatchObject({
type: "header",
text: { type: "plain_text", text: "✅ CPK-200" },
});
expect(json).toContain("✨ Filed in Linear");
// The Filed banner sits before the fields grid.
const bannerIdx = blocks.findIndex(
(b) =>
b.type === "context" && JSON.stringify(b).includes("Filed in Linear"),
);
const fieldsIdx = blocks.findIndex(
(b) => b.type === "section" && "fields" in b,
);
expect(bannerIdx).toBeGreaterThan(-1);
expect(bannerIdx).toBeLessThan(fieldsIdx);
// unassigned fallback + Status placeholder grid still render.
expect(json).toContain("_unassigned_");
// No priority/state → Linear purple.
expect(accent).toBe("#5E6AD2");
});
it("appends a divider + trimmed description when present", () => {
const long = "x".repeat(700);
const { blocks } = renderSlackMessage(
renderToIR(
<IssueCard identifier="CPK-300" title="Big" description={long} />,
),
);
expect(blocks.filter((b) => b.type === "divider")).toHaveLength(1);
const descSection = blocks[blocks.length - 1] as {
text?: { text: string };
};
// Description is trimmed to 600 chars + an ellipsis.
expect(descSection.text?.text).toBe(`${"x".repeat(600)}`);
});
});
describe("PageList component", () => {
it("renders linked titles, snippets and a count footer", () => {
const { blocks, accent } = renderSlackMessage(
renderToIR(
<PageList
heading="Runbooks"
pages={[
{
title: "Auth outage runbook",
url: "https://www.notion.so/abc",
snippet: "Steps to mitigate auth provider downtime.",
edited: "3d ago",
},
{ title: "No-link page" },
]}
/>,
),
);
const json = JSON.stringify(blocks);
expect(blocks[0]).toMatchObject({
type: "header",
text: { type: "plain_text", text: "📚 Runbooks" },
});
expect(json).toContain("<https://www.notion.so/abc|*Auth outage runbook*>");
expect(json).toContain("Steps to mitigate auth provider downtime.");
expect(json).toContain("🕒 edited 3d ago");
// A page without a url renders as bold text rather than a link.
expect(json).toContain("*No-link page*");
expect(json).not.toContain("|*No-link page*>");
expect(json).toContain("2 pages");
// Exactly one divider between the two pages.
expect(blocks.filter((b) => b.type === "divider")).toHaveLength(1);
// Notion-dark accent.
expect(accent).toBe("#2F3437");
});
});
// ── Telegram parity tests ────────────────────────────────────────────────────
// These tests render the same IR through renderTelegram and assert that the
// unicode status/priority glyphs appear correctly (no Slack `:shortcode:`
// strings that Telegram would not expand).
describe("IssueCard Telegram parity", () => {
it("renders unicode status and priority glyphs in Telegram output", () => {
const payload = renderTelegram(
renderToIR(
<IssueCard
identifier="CPK-101"
title="Checkout 500s under load"
url="https://linear.app/copilotkit/issue/CPK-101"
state="In Progress"
assignee="Alem"
priority="Urgent"
team="CPK"
/>,
),
);
// renderTelegram returns a TelegramPayload with a `text` field (HTML string)
// and `parseMode: "HTML"` — confirmed from telegram.test.ts line:
// expect(out.parseMode).toBe("HTML");
// expect(out.text).toContain("<b>Status</b>");
expect(typeof payload.text).toBe("string");
// In-progress maps to the blue dot unicode glyph.
expect(payload.text).toContain("🔵");
// Urgent priority maps to the siren glyph.
expect(payload.text).toContain("🚨");
// Identifier and title text must appear in the output.
expect(payload.text).toContain("CPK-101");
expect(payload.text).toContain("Checkout 500s under load");
// No Slack mrkdwn shortcodes must appear.
expect(payload.text).not.toContain(":large_blue_circle:");
expect(payload.text).not.toContain(":rotating_light:");
});
it("renders 'done' unicode glyph for justCreated issue in Telegram output", () => {
const payload = renderTelegram(
renderToIR(
<IssueCard identifier="CPK-200" title="New bug" justCreated />,
),
);
expect(typeof payload.text).toBe("string");
// justCreated uses the check-mark glyph.
expect(payload.text).toContain("✅");
expect(payload.text).toContain("CPK-200");
expect(payload.text).toContain("New bug");
});
});
describe("IssueList Telegram parity", () => {
it("renders unicode status glyphs for each issue in Telegram output", () => {
const payload = renderTelegram(
renderToIR(
<IssueList
heading="Open"
issues={[
{
identifier: "CPK-101",
title: "Checkout 500s under load",
url: "https://linear.app/copilotkit/issue/CPK-101",
state: "In Progress",
assignee: "Alem",
priority: "Urgent",
updated: "2d ago",
},
{
identifier: "CPK-102",
title: "Login redirect loop",
url: "https://linear.app/copilotkit/issue/CPK-102",
state: "Todo",
assignee: "Sam",
priority: "High",
updated: "5h ago",
},
]}
/>,
),
);
expect(typeof payload.text).toBe("string");
// In-progress maps to the blue dot.
expect(payload.text).toContain("🔵");
// Todo/unknown maps to the orange dot.
expect(payload.text).toContain("🟠");
// Identifiers must be present.
expect(payload.text).toContain("CPK-101");
expect(payload.text).toContain("CPK-102");
// No Slack mrkdwn shortcodes.
expect(payload.text).not.toContain(":large_blue_circle:");
expect(payload.text).not.toContain(":large_orange_circle:");
});
});
describe("PageList Telegram parity", () => {
it("renders page titles and snippets in Telegram output", () => {
const payload = renderTelegram(
renderToIR(
<PageList
heading="Runbooks"
pages={[
{
title: "Auth outage runbook",
url: "https://www.notion.so/abc",
snippet: "Steps to mitigate auth provider downtime.",
edited: "3d ago",
},
]}
/>,
),
);
expect(typeof payload.text).toBe("string");
expect(payload.text).toContain("Auth outage runbook");
expect(payload.text).toContain("Steps to mitigate auth provider downtime.");
});
});
+65
View File
@@ -0,0 +1,65 @@
/**
* Shared status/priority → glyph mapping for the Linear components. The
* functions return unicode glyphs (not Slack `:shortcode:` strings), so the
* components render identically on Slack and Telegram.
*/
/** Unicode glyph for a Linear workflow-state name. */
export function stateGlyph(state?: string): string {
const s = (state ?? "").toLowerCase();
if (s.includes("done") || s.includes("complete")) return "✅";
if (s.includes("progress") || s.includes("started")) return "🔵";
if (s.includes("review")) return "🟣";
if (s.includes("cancel")) return "🚫";
if (s.includes("backlog")) return "⚪";
return "🟠";
}
/** Unicode glyph for a Linear priority label, or "" for none/unknown. */
export function priorityGlyph(priority?: string): string {
const p = (priority ?? "").toLowerCase();
if (p.includes("urgent")) return "🚨";
if (p.includes("high")) return "🔴";
if (p.includes("medium")) return "🟠";
if (p.includes("low")) return "⚪";
return "";
}
/** Brand + semantic accent colors for the attachment left-border. */
export const ACCENT = {
linear: "#5E6AD2",
notion: "#2F3437",
urgent: "#EB5757",
high: "#F2994A",
done: "#27AE60",
progress: "#2D9CDB",
canceled: "#9B9B9B",
} as const;
/**
* Accent color for a single issue: priority wins (urgent/high), then state
* (done/in-progress/canceled), falling back to Linear purple.
*/
export function accentForIssue(issue: {
state?: string;
priority?: string;
}): string {
const p = (issue.priority ?? "").toLowerCase();
if (p.includes("urgent")) return ACCENT.urgent;
if (p.includes("high")) return ACCENT.high;
const s = (issue.state ?? "").toLowerCase();
if (s.includes("done") || s.includes("complete")) return ACCENT.done;
if (s.includes("cancel")) return ACCENT.canceled;
if (s.includes("progress") || s.includes("started")) return ACCENT.progress;
return ACCENT.linear;
}
/** Accent for a list: surface the hottest priority present, else Linear purple. */
export function accentForIssues(
issues: ReadonlyArray<{ priority?: string }>,
): string {
const prios = issues.map((i) => (i.priority ?? "").toLowerCase());
if (prios.some((p) => p.includes("urgent"))) return ACCENT.urgent;
if (prios.some((p) => p.includes("high"))) return ACCENT.high;
return ACCENT.linear;
}
+16
View File
@@ -0,0 +1,16 @@
/**
* App-specific render components — agent-renderable Block Kit cards authored
* with the `@copilotkit/channels-ui` JSX vocabulary.
*
* Each component is a plain `ComponentFn` returning a `<Message>` tree; its
* exported zod prop schema doubles as the render-tool input schema. Render a
* component with `renderSlackMessage(renderToIR(<IssueCard {...props} />))`.
*/
export { IssueCard, issueCardSchema } from "./issue-card.js";
export type { IssueCardProps } from "./issue-card.js";
export { IssueList, issueListSchema } from "./issue-list.js";
export type { IssueListProps } from "./issue-list.js";
export { PageList, pageListSchema } from "./page-list.js";
export type { PageListProps } from "./page-list.js";
@@ -0,0 +1,96 @@
/**
* `issue_card` — a rich single-issue card: a header with the status + id,
* the title as a link, a two-column metadata grid (status / assignee /
* priority / team / cycle / updated), an optional description, and an
* optional labels + "Open in Linear" footer.
*
* Use it for one issue — when the user asks about a specific issue, or
* right after creating one (it doubles as the "filed!" confirmation).
*
* Authored with the `@copilotkit/channels-ui` JSX vocabulary; the Block Kit
* shapes are produced by `renderSlackMessage(renderToIR(<IssueCard .../>))`.
*/
import { z } from "zod";
import {
Context,
Divider,
Fields,
Field,
Header,
Message,
Section,
} from "@copilotkit/channels-ui";
import type { BotNode } from "@copilotkit/channels-ui";
import { accentForIssue, priorityGlyph, stateGlyph } from "./_status.js";
export const issueCardSchema = z.object({
identifier: z.string().describe("Issue identifier, e.g. 'CPK-1234'."),
title: z.string().describe("Issue title."),
url: z.string().optional().describe("Link to the issue in Linear."),
state: z.string().optional().describe("Workflow state name."),
assignee: z.string().optional().describe("Assignee display name."),
priority: z.string().optional().describe("Priority label."),
team: z.string().optional().describe("Team key/name, e.g. 'CPK'."),
cycle: z.string().optional().describe("Cycle name/number."),
updated: z.string().optional().describe("Human-readable last-updated."),
description: z
.string()
.optional()
.describe(
"Issue description (markdown). Kept short; long text is trimmed.",
),
labels: z.array(z.string()).optional().describe("Label names."),
justCreated: z
.boolean()
.optional()
.describe(
"Set true right after creating the issue to show a 'Filed' banner.",
),
});
export type IssueCardProps = z.infer<typeof issueCardSchema>;
/** Render ONE Linear issue as a rich Block Kit card. */
export function IssueCard(issue: IssueCardProps): BotNode {
const titleText = issue.url
? `[**${issue.title}**](${issue.url})`
: `**${issue.title}**`;
const prio = priorityGlyph(issue.priority);
const description = issue.description
? issue.description.length > 600
? `${issue.description.slice(0, 600)}`
: issue.description
: undefined;
const footer: string[] = [];
if (issue.labels?.length) footer.push(`🏷️ ${issue.labels.join(" ")}`);
if (issue.url) footer.push(`[Open in Linear →](${issue.url})`);
const footerText = footer.length ? footer.join(" · ") : undefined;
return (
<Message accent={accentForIssue(issue)}>
<Header>
{`${issue.justCreated ? "✅ " : `${stateGlyph(issue.state)} `}${issue.identifier}`}
</Header>
<Section>{titleText}</Section>
{issue.justCreated ? <Context>{"✨ Filed in Linear"}</Context> : null}
<Fields>
<Field>{`**Status**\n${stateGlyph(issue.state)} ${issue.state ?? "—"}`}</Field>
<Field>{`**Assignee**\n${issue.assignee ?? "_unassigned_"}`}</Field>
{issue.priority ? (
<Field>{`**Priority**\n${prio ? `${prio} ` : ""}${issue.priority}`}</Field>
) : null}
{issue.team ? <Field>{`**Team**\n${issue.team}`}</Field> : null}
{issue.cycle ? <Field>{`**Cycle**\n${issue.cycle}`}</Field> : null}
{issue.updated ? (
<Field>{`**Updated**\n${issue.updated}`}</Field>
) : null}
</Fields>
{description ? <Divider /> : null}
{description ? <Section>{description}</Section> : null}
{footerText ? <Context>{footerText}</Context> : null}
</Message>
);
}
@@ -0,0 +1,87 @@
/**
* `issue_list` — renders a set of Linear issues as a compact Block Kit card:
* a header, ONE section with one scannable line per issue (status dot + linked
* identifier + title + assignee · updated), and a count footer.
*
* This is deliberately a fixed THREE-block layout (header + section + context)
* regardless of issue count: a card-per-issue layout (~3 blocks each) blows
* past Slack's per-attachment block limit on long lists and gets rejected with
* `invalid_attachments`. We instead inline up to `MAX` issues into a single
* section and surface the overflow in the footer.
*
* The agent fetches issues from the Linear MCP server and passes the fields
* it wants shown; the Slack formatting lives here. For a single issue (or
* right after creating one) prefer `issue_card`, which shows a full grid.
*
* Authored with the `@copilotkit/channels-ui` JSX vocabulary.
*/
import { z } from "zod";
import { Context, Header, Message, Section } from "@copilotkit/channels-ui";
import type { BotNode } from "@copilotkit/channels-ui";
import { accentForIssues, stateGlyph } from "./_status.js";
const issueSchema = z.object({
identifier: z.string().describe("Linear issue identifier, e.g. 'CPK-1234'."),
title: z.string().describe("Issue title."),
url: z.string().optional().describe("Link to the issue in Linear."),
state: z
.string()
.optional()
.describe("Workflow state name, e.g. 'Todo', 'In Progress', 'Done'."),
assignee: z
.string()
.optional()
.describe("Assignee display name, or omit if unassigned."),
priority: z
.string()
.optional()
.describe("Priority label, e.g. 'Urgent', 'High', 'Medium', 'Low'."),
updated: z
.string()
.optional()
.describe("Human-readable last-updated, e.g. '2d ago'."),
});
export const issueListSchema = z.object({
heading: z
.string()
.optional()
.describe("Optional heading, e.g. 'Open CPK issues this cycle'."),
issues: z.array(issueSchema).min(1).describe("The issues to render."),
});
export type IssueListProps = z.infer<typeof issueListSchema>;
type Issue = z.infer<typeof issueSchema>;
/** Max issues rendered inline; the rest are summarized in the footer. */
const MAX = 15;
/** Max title length before trimming (keeps each line scannable). */
const TITLE_MAX = 70;
/** Render a list of Linear issues as a compact, fixed-size Block Kit card. */
export function IssueList({ heading, issues }: IssueListProps): BotNode {
const lines = issues.slice(0, MAX).map((issue: Issue) => {
const idLink = issue.url
? `[**${issue.identifier}**](${issue.url})`
: `**${issue.identifier}**`;
const title =
issue.title.length > TITLE_MAX
? `${issue.title.slice(0, TITLE_MAX)}`
: issue.title;
const meta = `${issue.assignee ?? "unassigned"}${issue.updated ? ` · ${issue.updated}` : ""}`;
return `${stateGlyph(issue.state)} ${idLink} ${title}${meta}`;
});
const footer =
issues.length > MAX
? `Showing ${MAX} of ${issues.length} issues`
: `${issues.length} issue${issues.length === 1 ? "" : "s"}`;
return (
<Message accent={accentForIssues(issues)}>
<Header>{`📋 ${heading ?? "Linear issues"}`}</Header>
<Section>{lines.join("\n")}</Section>
<Context>{footer}</Context>
</Message>
);
}
@@ -0,0 +1,76 @@
/**
* `page_list` — renders Notion page search results as a Block Kit card:
* a header, then one row per page (📄 linked title + a greyed snippet and
* optional last-edited), with dividers and a count footer.
*
* The agent searches Notion via MCP and passes the pages it wants to
* surface; the Slack formatting lives here.
*
* Authored with the `@copilotkit/channels-ui` JSX vocabulary.
*/
import { z } from "zod";
import {
Context,
Divider,
Header,
Message,
Section,
} from "@copilotkit/channels-ui";
import type { BotNode } from "@copilotkit/channels-ui";
import { ACCENT } from "./_status.js";
const pageSchema = z.object({
title: z.string().describe("Page title."),
url: z.string().optional().describe("Link to the page in Notion."),
snippet: z
.string()
.optional()
.describe("A short excerpt or summary of the page."),
editedBy: z.string().optional().describe("Who last edited it, if known."),
edited: z
.string()
.optional()
.describe("Human-readable last-edited, e.g. '3d ago'."),
});
export const pageListSchema = z.object({
heading: z
.string()
.optional()
.describe("Optional heading, e.g. 'Runbooks matching “auth outage”'."),
pages: z.array(pageSchema).min(1).describe("The pages to render."),
});
export type PageListProps = z.infer<typeof pageListSchema>;
type Page = z.infer<typeof pageSchema>;
/** Render a list of Notion pages as a Block Kit card. */
export function PageList({ heading, pages }: PageListProps): BotNode {
const rows: BotNode[] = [];
pages.forEach((page: Page, i: number) => {
const titleLink = page.url
? `[**${page.title}**](${page.url})`
: `**${page.title}**`;
const meta = [
page.snippet,
page.edited
? `🕒 edited ${page.edited}${page.editedBy ? ` by ${page.editedBy}` : ""}`
: null,
]
.filter(Boolean)
.join("\n");
rows.push(<Section>{`📄 ${titleLink}`}</Section>);
if (meta) rows.push(<Context>{meta}</Context>);
if (i < pages.length - 1) rows.push(<Divider />);
});
return (
<Message accent={ACCENT.notion}>
<Header>{`📚 ${heading ?? "Notion pages"}`}</Header>
{rows}
<Context>{`${pages.length} page${pages.length === 1 ? "" : "s"}`}</Context>
</Message>
);
}