Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/renderer/components/thread/ChatPane/ChatPane.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,21 @@ describe("ChatPane", () => {
).not.toBeInTheDocument();
});

it("renders selected skills in user messages as skill badges", async () => {
const thread = makeThread();
seedUserMessageContent(thread.id, [
{ kind: "skill", name: "simplify", invocation: "$simplify" },
]);

const { container } = renderChatPane(thread);
await waitFor(() => expect(hydrateThreadRuntimeItems).toHaveBeenCalledWith(thread.id));

const badge = container.querySelector('[data-skill-name="simplify"]');
expect(badge).toHaveTextContent("simplify");
expect(badge?.querySelector("svg")).toBeInTheDocument();
expect(screen.queryByText("$simplify")).not.toBeInTheDocument();
});

it("copies user message text from the inline action", async () => {
const thread = makeThread();
seedUserMessage(thread.id, "Copy this prompt");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { memo, type ReactNode, useEffectEvent, useLayoutEffect, useRef, useState } from "react";
import { Link, Surface, Tooltip, toast } from "@heroui/react";
import { useLingui } from "@lingui/react/macro";
import { ChevronDown, ChevronUp, Copy } from "lucide-react";
import { ChevronDown, ChevronUp, Copy, Sparkles } from "lucide-react";
import type { CanonicalContentBlock, MessageItemPayload } from "@/shared/contracts";
import { friendlyError } from "@/shared/messages";
import { AttachmentBar } from "@/renderer/components/composer/AttachmentBar";
Expand Down Expand Up @@ -66,8 +66,8 @@ export const UserMessage = memo(function UserMessage({ item, checkpointRevert }:
const { slashCommand, body } = extractLeadingSlashCommand(rawText);
const text = body;
const commandPrefixLength = slashCommand ? rawText.length - body.length : 0;
const hasInlineFileMentions = content.some(
(block) => block.kind === "file" && block.source !== "attachment",
const hasInlineContent = content.some(
(block) => block.kind === "skill" || (block.kind === "file" && block.source !== "attachment"),
);
const attachments = enrichWithSelectorPayloads(
buildUserPromptAttachments(content),
Expand Down Expand Up @@ -205,14 +205,11 @@ export const UserMessage = memo(function UserMessage({ item, checkpointRevert }:
bodyClass = inlineBodyClass;
bodyContent = (
<>
<span className="poracode-slash-chip poracode-slash-chip--user-message mr-1.5">
<span className="poracode-slash-chip__slash">/</span>
<span className="poracode-slash-chip__name">{slashCommand}</span>
</span>
<UserMessageSlashChip icon="/" label={slashCommand} />
{renderUserMessageInlineContent(content, commandPrefixLength, actions)}
</>
);
} else if (hasInlineFileMentions) {
} else if (hasInlineContent) {
bodyClass = inlineBodyClass;
bodyContent = renderUserMessageInlineContent(content, 0, actions);
} else if (text.length > 0) {
Expand Down Expand Up @@ -351,6 +348,7 @@ function buildUserPromptText(content: CanonicalContentBlock[]): string {
return content
.map((block) => {
if (block.kind === "text") return block.text;
if (block.kind === "skill") return block.invocation;
if (block.kind === "file" && block.source !== "attachment") return block.path;
return "";
})
Expand All @@ -377,6 +375,23 @@ function renderUserMessageInlineContent(
return;
}

if (block.kind === "skill") {
if (remainingSkip >= block.invocation.length) {
remainingSkip -= block.invocation.length;
return;
}
remainingSkip = 0;
nodes.push(
<UserMessageSlashChip
key={`skill-${index}-${block.name}`}
icon={<Sparkles aria-hidden="true" />}
label={block.name}
skillName={block.name}
/>,
);
return;
}

if (block.kind === "file") {
if (block.source === "attachment") return;
if (remainingSkip >= block.path.length) {
Expand All @@ -400,6 +415,26 @@ function renderUserMessageInlineContent(
return nodes;
}

function UserMessageSlashChip({
icon,
label,
skillName,
}: {
icon: ReactNode;
label: string;
skillName?: string;
}) {
return (
<span
className="poracode-slash-chip poracode-slash-chip--user-message mr-1.5"
{...(skillName ? { "data-skill-name": skillName } : {})}
>
<span className="poracode-slash-chip__slash">{icon}</span>
<span className="poracode-slash-chip__name">{label}</span>
</span>
);
}

const USER_MESSAGE_URL_RE = /https?:\/\/[^\s<>"']+/g;

function renderUserMessageText(text: string, keyPrefix: string): ReactNode[] {
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2492,6 +2492,10 @@ html[data-app-unfocused] .poracode-provider-icon--finished {
cursor: default;
}

.poracode-slash-chip[data-skill-name] {
gap: 0.25em;
}

.poracode-slash-chip__slash {
display: inline-flex;
align-items: center;
Expand Down
1 change: 1 addition & 0 deletions src/shared/contracts/runtimeEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type RuntimeContentStreamKind = z.infer<typeof runtimeContentStreamKindSc

export const canonicalContentBlockSchema = z.discriminatedUnion("kind", [
z.object({ kind: z.literal("text"), text: z.string() }),
z.object({ kind: z.literal("skill"), name: z.string(), invocation: z.string() }),
z.object({
kind: z.literal("image"),
mimeType: z.string(),
Expand Down
10 changes: 8 additions & 2 deletions src/shared/promptContent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe("buildPromptContentBlocks", () => {
]);
});

it("formats skill segments with the provider invocation", () => {
it("preserves skill segments for user-message rendering", () => {
expect(
buildPromptContentBlocks("Use the review-code skill.", [
{
Expand All @@ -54,7 +54,13 @@ describe("buildPromptContentBlocks", () => {
scope: "global",
},
]),
).toEqual([{ kind: "text", text: "Use the review-code skill." }]);
).toEqual([
{
kind: "skill",
name: "review-code",
invocation: "Use the review-code skill.",
},
]);
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/shared/promptContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export function buildPromptContentBlocks(
}

if (segment.kind === "skill") {
content.push({ kind: "text", text: segment.invocation });
content.push({ kind: "skill", name: segment.name, invocation: segment.invocation });
continue;
}

Expand Down