Render skill invocations compactly

This commit is contained in:
Federico Jaramillo Martinez
2026-05-07 16:16:02 +02:00
parent 685ad9882b
commit a987c36b2a
4 changed files with 36 additions and 0 deletions
+1
View File
@@ -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");
}
+24
View File
@@ -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(/^<skill name="([^"]+)" location="([^"]+)">\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;
+7
View File
@@ -129,6 +129,13 @@ export class ChatView extends LitElement {
if (part.type === "text" && message?.role === "bash") return html`<pre class="part shell-output">${part.text}</pre>`;
if (part.type === "text") return html`<formatted-text class="part" .text=${part.text}></formatted-text>`;
if (part.type === "thinking") return html`<details class="part"><summary>thinking</summary><formatted-text .text=${part.text}></formatted-text></details>`;
if (part.type === "skillInvocation") return html`
<details class="part skill-invocation">
<summary><b>[skill]</b> ${part.name}</summary>
<small>${part.location}</small>
<formatted-text .text=${part.content}></formatted-text>
</details>
`;
if (part.type === "toolCall") return html`<div class="part tool-line">▶ ${part.toolName}<span class="summary">${part.summary}</span></div>`;
if (part.type === "toolResult") return html`
<details class="part" ?open=${part.isError}>
+4
View File
@@ -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; }