97 lines
3.6 KiB
TypeScript
97 lines
3.6 KiB
TypeScript
/**
|
|
* `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>
|
|
);
|
|
}
|