diff --git a/ui/src/ui/views/skill-workshop.ts b/ui/src/ui/views/skill-workshop.ts index 812e9306ff5..4d35162b76f 100644 --- a/ui/src/ui/views/skill-workshop.ts +++ b/ui/src/ui/views/skill-workshop.ts @@ -108,6 +108,9 @@ const STATUS_LABEL: Record = { stale: "Stale", }; +const TODAY_PREVIEW_MAX_ITEMS = 3; +const TODAY_PREVIEW_MAX_ITEM_CHARS = 120; + const GROUP_LABEL: Record = { today: "Today", yesterday: "Yesterday", @@ -710,31 +713,136 @@ function renderToday( } function renderTodayDoesBlock(hero: SkillWorkshopProposal) { - const bullets = extractDoesBullets(hero.body); - if (bullets.length === 0) { + const preview = extractTodayProposalPreview(hero.body); + if (!preview) { return nothing; } return html`
-
What it'll do when you trigger it
+
${preview.heading}
    - ${bullets.slice(0, 5).map((b) => html`
  • ${renderInline(b)}
  • `)} + ${preview.items.map((item) => html`
  • ${item}
  • `)}
`; } -function extractDoesBullets(body: string): string[] { - const lines = body.split("\n"); +type TodayProposalPreview = { + heading: string; + items: string[]; +}; + +type ProposalBodySection = { + title: string; + lines: string[]; +}; + +function extractTodayProposalPreview(body: string): TodayProposalPreview | null { + const sections = splitProposalBodySections(body); + const workflow = findProposalSection(sections, [ + "workflow", + "procedure", + "steps", + "agent workflow", + "process", + ]); + const workflowItems = workflow ? extractTopLevelListItems(workflow.lines) : []; + if (workflowItems.length > 0) { + return { + heading: "How the agent will use it", + items: workflowItems.slice(0, TODAY_PREVIEW_MAX_ITEMS), + }; + } + + const applicability = findProposalSection(sections, [ + "when to use", + "use when", + "applies when", + "trigger", + "triggers", + ]); + const applicabilityItems = applicability ? extractTopLevelListItems(applicability.lines) : []; + if (applicabilityItems.length > 0) { + return { + heading: "When the agent should use it", + items: applicabilityItems.slice(0, TODAY_PREVIEW_MAX_ITEMS), + }; + } + + return null; +} + +function splitProposalBodySections(body: string): ProposalBodySection[] { + const sections: ProposalBodySection[] = []; + let current: ProposalBodySection | null = null; + let inCode = false; + + for (const raw of body.split("\n")) { + const trimmed = raw.trim(); + if (trimmed.startsWith("```")) { + inCode = !inCode; + } + const heading = !inCode ? /^(#{2,4})\s+(.+?)\s*$/.exec(trimmed) : null; + if (heading) { + current = { title: normalizeSectionTitle(heading[2]), lines: [] }; + sections.push(current); + continue; + } + current?.lines.push(raw); + } + + return sections; +} + +function findProposalSection( + sections: readonly ProposalBodySection[], + names: readonly string[], +): ProposalBodySection | undefined { + const wanted = new Set(names.map(normalizeSectionTitle)); + return sections.find((section) => wanted.has(section.title)); +} + +function normalizeSectionTitle(title: string): string { + return title + .replace(/[#*_`[\]().:]/g, " ") + .replace(/\s+/g, " ") + .trim() + .toLowerCase(); +} + +function extractTopLevelListItems(lines: readonly string[]): string[] { const out: string[] = []; for (const raw of lines) { + if (/^\s{2,}/.test(raw)) { + continue; + } const line = raw.trim(); const m = /^(?:[-*]|\d+\.)\s+(.+)/.exec(line); if (m) { - out.push(m[1].replace(/^\*\*[^*]+\*\*\s*/, "")); + out.push(cleanTodayPreviewItem(m[1])); } } - return out; + return out.filter(Boolean); +} + +function cleanTodayPreviewItem(item: string): string { + const cleaned = item + .replace(/^\*\*[^*]+\*\*\s*/, "") + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replace(/`([^`]+)`/g, "$1") + .replace(/\s+/g, " ") + .trim(); + return truncateAtWord(cleaned, TODAY_PREVIEW_MAX_ITEM_CHARS); +} + +function truncateAtWord(value: string, maxChars: number): string { + if (value.length <= maxChars) { + return value; + } + const clipped = value.slice(0, maxChars - 1); + const boundary = clipped.lastIndexOf(" "); + const base = boundary > 48 ? clipped.slice(0, boundary) : clipped; + return `${base.trimEnd()}…`; } function formatTodayDate(ms: number): string {