diff --git a/src/client/src/chatGroups.ts b/src/client/src/chatGroups.ts index 3dd268a..e08a1ef 100644 --- a/src/client/src/chatGroups.ts +++ b/src/client/src/chatGroups.ts @@ -47,5 +47,6 @@ export function summarizeChatGroup(messages: ChatLine[]): string { function isReadablePart(message: ChatLine, part: ChatPart): boolean { if (message.source === "compaction" || message.source === "branch_summary") return false; + if (part.type === "skillInvocation") return true; return part.type === "text" && (message.role === "user" || message.role === "assistant" || message.role === "system" || message.role === "bash"); } diff --git a/src/client/src/chatMessages.ts b/src/client/src/chatMessages.ts index 1158208..3bf27ed 100644 --- a/src/client/src/chatMessages.ts +++ b/src/client/src/chatMessages.ts @@ -24,6 +24,8 @@ function normalizeMessage(message: unknown): ChatLine[] { if (getString(message, "role") === "bashExecution") return [normalizeBashExecution(message)]; const role = normalizeRole(getString(message, "role")); const parts = normalizeContent(getProperty(message, "content"), message); + const skillLines = role === "user" ? normalizeSkillInvocation(parts) : undefined; + if (skillLines !== undefined) return skillLines; const source = normalizeSource(message, parts); if (role === "tool") return [{ role, parts, ...(source === undefined ? {} : { source }) }]; @@ -31,6 +33,28 @@ function normalizeMessage(message: unknown): ChatLine[] { return visible.length > 0 ? [{ role, parts: visible, ...(source === undefined ? {} : { source }) }] : []; } +function normalizeSkillInvocation(parts: ChatPart[]): ChatLine[] | undefined { + if (parts.length !== 1 || parts[0]?.type !== "text") return undefined; + const skill = parseSkillBlock(parts[0].text); + if (skill === undefined) return undefined; + return [ + { role: "user", parts: [{ type: "skillInvocation", name: skill.name, location: skill.location, content: skill.content }] }, + ...(skill.userMessage === undefined ? [] : [{ role: "user" as const, parts: [{ type: "text" as const, text: skill.userMessage }] }]), + ]; +} + +function parseSkillBlock(text: string): { name: string; location: string; content: string; userMessage?: string } | undefined { + const match = text.match(/^\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/); + if (match === null) return undefined; + const userMessage = match[4]?.trim(); + return { + name: match[1] ?? "skill", + location: match[2] ?? "", + content: match[3] ?? "", + ...(userMessage === undefined || userMessage === "" ? {} : { userMessage }), + }; +} + function normalizeSource(message: unknown, _parts: ChatPart[]): ChatLine["source"] | undefined { const source = getString(message, "source"); if (source === "compaction" || source === "branch_summary") return source; diff --git a/src/client/src/components/ChatView.ts b/src/client/src/components/ChatView.ts index 44f4681..a0ea093 100644 --- a/src/client/src/components/ChatView.ts +++ b/src/client/src/components/ChatView.ts @@ -129,6 +129,13 @@ export class ChatView extends LitElement { if (part.type === "text" && message?.role === "bash") return html`
${part.text}
`; if (part.type === "text") return html``; if (part.type === "thinking") return html`
thinking
`; + if (part.type === "skillInvocation") return html` +
+ [skill] ${part.name} + ${part.location} + +
+ `; if (part.type === "toolCall") return html`
▶ ${part.toolName}${part.summary}
`; if (part.type === "toolResult") return html`
diff --git a/src/client/src/components/shared.ts b/src/client/src/components/shared.ts index 218324c..ad06ab5 100644 --- a/src/client/src/components/shared.ts +++ b/src/client/src/components/shared.ts @@ -3,6 +3,7 @@ import { css } from "lit"; export type ChatPart = | { type: "text"; text: string } | { type: "thinking"; text: string } + | { type: "skillInvocation"; name: string; location: string; content: string } | { type: "toolCall"; toolName: string; summary: string } | { type: "toolResult"; toolName: string; text: string; isError: boolean } | { type: "empty" }; @@ -75,6 +76,9 @@ export const chatStyles = css` .tool-line { color: #d29922; } .summary { color: #8b949e; margin-left: 6px; } .part:is(details) { border-top: 1px solid #30363d; padding-top: 8px; } + .skill-invocation { border: 1px solid #30363d; border-radius: 8px; background: #161b22; padding: 8px 10px; } + .skill-invocation > summary { color: #d2a8ff; } + .skill-invocation > small { display: block; margin: 6px 0 8px; color: #8b949e; } summary { cursor: pointer; color: #8b949e; } pre { margin: 6px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; } .shell-output { color: #e6edf3; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; }