Skip to content

Commit fb3b85b

Browse files
committed
fix(chat): drop /·$ prefix from command and skill badges in message transcript
Command and skill badges in the sent-message transcript now display the bare name without the leading /·$ prefix, matching the composer's inline badges. The wire invocation token (/command, $skill) is unchanged, so agents still receive the literal text.
1 parent 758f502 commit fb3b85b

5 files changed

Lines changed: 31 additions & 18 deletions

File tree

src/components/chat/composer/reference-uri.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,21 +114,25 @@ describe("parseCodegReferenceUri", () => {
114114
})
115115
})
116116

117-
it("parses a skill uri, keeping the literal `/`·`$` token as the label", () => {
117+
it("parses a skill uri, stripping the leading `/`·`$` from the label", () => {
118118
expect(
119119
parseCodegReferenceUri("codeg://skill/review", "/review")
120120
).toMatchObject({
121121
refType: "skill",
122122
id: "review",
123-
label: "/review",
123+
label: "review",
124124
uri: "codeg://skill/review",
125125
meta: null,
126126
})
127+
// The `$` prefix ($skill / Codex expert) is stripped the same way.
128+
expect(
129+
parseCodegReferenceUri("codeg://skill/deploy", "$deploy")?.label
130+
).toBe("deploy")
127131
})
128132

129-
it("falls back to a /-prefixed id for an empty skill label", () => {
133+
it("falls back to the bare id for an empty skill label", () => {
130134
expect(parseCodegReferenceUri("codeg://skill/deploy", "")?.label).toBe(
131-
"/deploy"
135+
"deploy"
132136
)
133137
})
134138

src/components/chat/composer/reference-uri.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ const AGENT_URI = /^codeg:\/\/agent\/(.+)$/i
1111
const SESSION_URI = /^codeg:\/\/session\/(.+)$/i
1212
const COMMIT_URI = /^codeg:\/\/commit\/.*@(.+)$/i
1313
// command / skill / expert tokens, surfaced as badges in transcript user messages
14-
// (message/user-message-segments.ts). The label carries the literal `/`·`$` prefix.
14+
// (message/user-message-segments.ts). The label arrives with the literal `/`·`$`
15+
// prefix, which the skill branch strips so the badge reads the bare name.
1516
const SKILL_URI = /^codeg:\/\/skill\/(.+)$/i
1617

1718
// A path-less attached file (local-desktop paste/drop of inline bytes — an
@@ -118,10 +119,13 @@ export function parseCodegReferenceUri(
118119
}
119120
return {
120121
refType: "skill",
121-
// The link text keeps the literal token (`/build` / `$deploy`); fall back
122-
// to a `/`-prefixed id only if it was somehow empty.
122+
// The link text carries the literal invocation token (`/build` / `$deploy`);
123+
// strip a single leading `/`·`$` so the badge reads `build` — matching the
124+
// composer's live-inserted command/skill badge (whose label is the bare,
125+
// prefix-less name), the way the agent branch above strips a leading `@`.
126+
// Fall back to the bare id when the label is empty.
123127
id,
124-
label: label || `/${id}`,
128+
label: (label || id).replace(/^[/$]/, "") || id,
125129
uri,
126130
meta: null,
127131
}

src/components/message/plain-text-with-badges.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ describe("PlainTextWithBadges", () => {
5454
<PlainTextWithBadges text="run /review please" />
5555
)
5656
expect(badge(cmd, "skill")).not.toBeNull()
57-
expect(cmd.textContent).toContain("/review")
57+
// The badge shows the bare name (no `/` prefix), matching the composer.
58+
expect(cmd.textContent).toContain("review")
59+
expect(cmd.textContent).not.toContain("/review")
5860

5961
const { container: path } = render(
6062
<PlainTextWithBadges text="see /usr/bin for it" />

src/components/message/user-message-segments.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,19 +95,20 @@ describe("parseUserMessageSegments", () => {
9595
})
9696

9797
describe("bare invocation tokens → skill badges", () => {
98-
it("badges a /command token keeping its prefix", () => {
98+
it("badges a /command token, dropping its prefix from the label", () => {
9999
const segments = parseUserMessageSegments("run /review please")
100100
expect(segments[0]).toEqual({ kind: "text", text: "run " })
101101
const skill = segments[1] as { kind: "reference"; attrs: ReferenceAttrs }
102102
expect(skill.attrs.refType).toBe("skill")
103-
expect(skill.attrs.label).toBe("/review")
103+
// Badge label matches the composer's inline badge: the bare name, no `/`.
104+
expect(skill.attrs.label).toBe("review")
104105
expect(segments[2]).toEqual({ kind: "text", text: " please" })
105106
})
106107

107-
it("badges a $skill token", () => {
108+
it("badges a $skill token, dropping its prefix from the label", () => {
108109
const attrs = onlyReference("$deploy now")
109110
expect(attrs.refType).toBe("skill")
110-
expect(attrs.label).toBe("$deploy")
111+
expect(attrs.label).toBe("deploy")
111112
})
112113

113114
it("does NOT badge a file-ish path", () => {

src/components/message/user-message-segments.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ function unwrapDestination(destination: string): string {
3333

3434
/**
3535
* Split a plain-prose run into literal text and bare `/slug`·`$slug` skill
36-
* badges. Mirrors `rehype-command-badges` (same {@link INVOCATION_TOKEN_RE}) so a
37-
* sent invocation token renders identically to how the assistant-side transcript
38-
* used to badge it — the skill badge label keeps the literal `/`·`$` prefix.
36+
* badges (same {@link INVOCATION_TOKEN_RE} the composer's `/`·`$` triggers use).
37+
* The badge label drops the literal `/`·`$` prefix (the parser strips it) so a
38+
* sent invocation token renders identically to the composer's inline badge,
39+
* which shows the bare command/skill name.
3940
*/
4041
function pushProseSegments(value: string, out: UserMessageSegment[]): void {
4142
INVOCATION_TOKEN_RE.lastIndex = 0
@@ -48,8 +49,9 @@ function pushProseSegments(value: string, out: UserMessageSegment[]): void {
4849
out.push({ kind: "text", text: value.slice(lastIndex, tokenStart) })
4950
}
5051
const slug = token.slice(1)
51-
// Resolve through the same parser the transcript uses for `codeg://skill/…`
52-
// so the badge label keeps the literal token (`/build`, `$deploy`).
52+
// Resolve through the shared reference parser, which strips the leading
53+
// `/`·`$` so the badge label is the bare slug (`build`, `deploy`) — matching
54+
// the composer's inline command/skill badge.
5355
const attrs = parseCodegReferenceUri(
5456
`codeg://skill/${encodeURIComponent(slug)}`,
5557
token

0 commit comments

Comments
 (0)