Skip to content

Commit c0fa042

Browse files
committed
feat: enhance slash command processing and summary generation in mentions
1 parent eb395b4 commit c0fa042

6 files changed

Lines changed: 316 additions & 109 deletions

File tree

src/__tests__/command-mentions.spec.ts

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe("Command Mentions", () => {
2727

2828
// Helper function to call parseMentions with required parameters
2929
const callParseMentions = async (text: string) => {
30-
const result = await parseMentions(
30+
return parseMentions(
3131
text,
3232
"/test/cwd", // cwd
3333
mockUrlContentFetcher, // urlContentFetcher
@@ -38,8 +38,6 @@ describe("Command Mentions", () => {
3838
50, // maxDiagnosticMessages
3939
undefined, // maxReadFileLine
4040
)
41-
// Return just the text for backward compatibility with existing tests
42-
return result.text
4341
}
4442

4543
describe("parseMentions with command support", () => {
@@ -56,10 +54,10 @@ describe("Command Mentions", () => {
5654
const result = await callParseMentions(input)
5755

5856
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup")
59-
expect(result).toContain('<command name="setup">')
60-
expect(result).toContain(commandContent)
61-
expect(result).toContain("</command>")
62-
expect(result).toContain("Please help me set up the project")
57+
expect(result.slashCommandHelp).toContain('<command name="setup">')
58+
expect(result.slashCommandHelp).toContain(commandContent)
59+
expect(result.slashCommandHelp).toContain("</command>")
60+
expect(result.text).toContain("Please help me set up the project")
6361
})
6462

6563
it("should handle multiple commands in message", async () => {
@@ -99,10 +97,10 @@ describe("Command Mentions", () => {
9997
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup")
10098
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "deploy")
10199
expect(mockGetCommand).toHaveBeenCalledTimes(2) // Each unique command called once (optimized)
102-
expect(result).toContain('<command name="setup">')
103-
expect(result).toContain("# Setup Environment")
104-
expect(result).toContain('<command name="deploy">')
105-
expect(result).toContain("# Deploy Environment")
100+
expect(result.slashCommandHelp).toContain('<command name="setup">')
101+
expect(result.slashCommandHelp).toContain("# Setup Environment")
102+
expect(result.slashCommandHelp).toContain('<command name="deploy">')
103+
expect(result.slashCommandHelp).toContain("# Deploy Environment")
106104
})
107105

108106
it("should leave non-existent commands unchanged", async () => {
@@ -114,10 +112,10 @@ describe("Command Mentions", () => {
114112

115113
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "nonexistent")
116114
// The command should remain unchanged in the text
117-
expect(result).toBe("/nonexistent command")
115+
expect(result.text).toBe("/nonexistent command")
118116
// Should not contain any command tags
119-
expect(result).not.toContain('<command name="nonexistent">')
120-
expect(result).not.toContain("Command 'nonexistent' not found")
117+
expect(result.slashCommandHelp).toBeUndefined()
118+
expect(result.text).not.toContain("Command 'nonexistent' not found")
121119
})
122120

123121
it("should handle command loading errors during existence check", async () => {
@@ -129,8 +127,8 @@ describe("Command Mentions", () => {
129127

130128
// When getCommand throws an error during existence check,
131129
// the command is treated as non-existent and left unchanged
132-
expect(result).toBe("/error-command test")
133-
expect(result).not.toContain('<command name="error-command">')
130+
expect(result.text).toBe("/error-command test")
131+
expect(result.slashCommandHelp).toBeUndefined()
134132
})
135133

136134
it("should handle command loading errors during processing", async () => {
@@ -145,9 +143,9 @@ describe("Command Mentions", () => {
145143
const input = "/error-command test"
146144
const result = await callParseMentions(input)
147145

148-
expect(result).toContain('<command name="error-command">')
149-
expect(result).toContain("# Error command")
150-
expect(result).toContain("</command>")
146+
expect(result.slashCommandHelp).toContain('<command name="error-command">')
147+
expect(result.slashCommandHelp).toContain("# Error command")
148+
expect(result.slashCommandHelp).toContain("</command>")
151149
})
152150

153151
it("should handle command names with hyphens and underscores at start", async () => {
@@ -162,8 +160,8 @@ describe("Command Mentions", () => {
162160
const result = await callParseMentions(input)
163161

164162
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup-dev")
165-
expect(result).toContain('<command name="setup-dev">')
166-
expect(result).toContain("# Dev setup")
163+
expect(result.slashCommandHelp).toContain('<command name="setup-dev">')
164+
expect(result.slashCommandHelp).toContain("# Dev setup")
167165
})
168166

169167
it("should preserve command content formatting", async () => {
@@ -192,13 +190,13 @@ npm install
192190
const input = "/complex command"
193191
const result = await callParseMentions(input)
194192

195-
expect(result).toContain('<command name="complex">')
196-
expect(result).toContain("# Complex Command")
197-
expect(result).toContain("```bash")
198-
expect(result).toContain("npm install")
199-
expect(result).toContain("- Check file1.js")
200-
expect(result).toContain("> **Note**: This is important!")
201-
expect(result).toContain("</command>")
193+
expect(result.slashCommandHelp).toContain('<command name="complex">')
194+
expect(result.slashCommandHelp).toContain("# Complex Command")
195+
expect(result.slashCommandHelp).toContain("```bash")
196+
expect(result.slashCommandHelp).toContain("npm install")
197+
expect(result.slashCommandHelp).toContain("- Check file1.js")
198+
expect(result.slashCommandHelp).toContain("> **Note**: This is important!")
199+
expect(result.slashCommandHelp).toContain("</command>")
202200
})
203201

204202
it("should handle empty command content", async () => {
@@ -212,8 +210,8 @@ npm install
212210
const input = "/empty command"
213211
const result = await callParseMentions(input)
214212

215-
expect(result).toContain('<command name="empty">')
216-
expect(result).toContain("</command>")
213+
expect(result.slashCommandHelp).toContain('<command name="empty">')
214+
expect(result.slashCommandHelp).toContain("</command>")
217215
// Should still include the command tags even with empty content
218216
})
219217
})
@@ -295,7 +293,7 @@ npm install
295293
const input = "/setup the project"
296294
const result = await callParseMentions(input)
297295

298-
expect(result).toContain("Command 'setup' (see below for command content)")
296+
expect(result.text).toContain("Command 'setup' (see below for command content)")
299297
})
300298

301299
it("should leave non-existent command mentions unchanged", async () => {
@@ -304,7 +302,7 @@ npm install
304302
const input = "/nonexistent the project"
305303
const result = await callParseMentions(input)
306304

307-
expect(result).toBe("/nonexistent the project")
305+
expect(result.text).toBe("/nonexistent the project")
308306
})
309307

310308
it("should process multiple commands in message", async () => {
@@ -325,8 +323,8 @@ npm install
325323
const input = "/setup the project\nThen /deploy later"
326324
const result = await callParseMentions(input)
327325

328-
expect(result).toContain("Command 'setup' (see below for command content)")
329-
expect(result).toContain("Command 'deploy' (see below for command content)")
326+
expect(result.text).toContain("Command 'setup' (see below for command content)")
327+
expect(result.text).toContain("Command 'deploy' (see below for command content)")
330328
})
331329

332330
it("should match commands anywhere with proper word boundaries", async () => {
@@ -340,22 +338,22 @@ npm install
340338
// At the beginning - should match
341339
let input = "/build the project"
342340
let result = await callParseMentions(input)
343-
expect(result).toContain("Command 'build'")
341+
expect(result.text).toContain("Command 'build'")
344342

345343
// After space - should match
346344
input = "Please /build and test"
347345
result = await callParseMentions(input)
348-
expect(result).toContain("Command 'build'")
346+
expect(result.text).toContain("Command 'build'")
349347

350348
// At the end - should match
351349
input = "Run the /build"
352350
result = await callParseMentions(input)
353-
expect(result).toContain("Command 'build'")
351+
expect(result.text).toContain("Command 'build'")
354352

355353
// At start of new line - should match
356354
input = "Some text\n/build the project"
357355
result = await callParseMentions(input)
358-
expect(result).toContain("Command 'build'")
356+
expect(result.text).toContain("Command 'build'")
359357
})
360358
})
361359
})

src/core/condense/__tests__/condense.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,41 @@ describe("Condense", () => {
139139
expect(effectiveHistory[0].role).toBe("user")
140140
})
141141

142+
it("should include slash command content in the summary message", async () => {
143+
const messages: ApiMessage[] = [
144+
{
145+
role: "user",
146+
content: [
147+
{ type: "text", text: "Some user text" },
148+
{ type: "text", text: '<command name="prr">Help content</command>' },
149+
],
150+
},
151+
{ role: "assistant", content: "Second message" },
152+
{ role: "user", content: "Third message" },
153+
{ role: "assistant", content: "Fourth message" },
154+
{ role: "user", content: "Fifth message" },
155+
{ role: "assistant", content: "Sixth message" },
156+
{ role: "user", content: "Seventh message" },
157+
{ role: "assistant", content: "Eighth message" },
158+
{ role: "user", content: "Ninth message" },
159+
]
160+
161+
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
162+
163+
const summaryMessage = result.messages.find((msg) => msg.isSummary)
164+
const content = summaryMessage!.content as Anthropic.Messages.ContentBlockParam[]
165+
166+
// Should have: summary text block, slash command block, ...reminders
167+
expect(content[0]).toEqual({
168+
type: "text",
169+
text: expect.stringContaining("Mock summary of the conversation"),
170+
})
171+
expect(content[1]).toEqual({
172+
type: "text",
173+
text: '<system-reminder>\n<command name="prr">Help content</command>\n</system-reminder>',
174+
})
175+
})
176+
142177
it("should handle complex first message content", async () => {
143178
const complexContent: Anthropic.Messages.ContentBlockParam[] = [
144179
{ type: "text", text: "/mode code" },

src/core/condense/index.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,23 @@ export async function summarizeConversation(
350350
text: `${prefaceParagraph}\n\n${summary}`,
351351
}
352352

353+
// Check if the first message has a slash command block
354+
let slashCommandBlock: Anthropic.Messages.ContentBlockParam | undefined
355+
if (messages.length > 0) {
356+
const firstMessage = messages[0]
357+
if (firstMessage.role === "user" && Array.isArray(firstMessage.content)) {
358+
const foundBlock = firstMessage.content.find(
359+
(block) => block.type === "text" && block.text.includes("<command name="),
360+
)
361+
if (foundBlock && foundBlock.type === "text") {
362+
slashCommandBlock = {
363+
type: "text",
364+
text: `<system-reminder>\n${foundBlock.text}\n</system-reminder>`,
365+
}
366+
}
367+
}
368+
}
369+
353370
const reminderBlocks: Anthropic.Messages.TextBlockParam[] = reminders.map((msg) => {
354371
const tsValue = msg.ts ?? null
355372
const rawContentJson = JSON.stringify(msg.content)
@@ -367,10 +384,11 @@ export async function summarizeConversation(
367384
})
368385
}
369386

370-
const summaryContent: Anthropic.Messages.ContentBlockParam[] = [summaryTextBlock, ...reminderBlocks].slice(
371-
0,
372-
1 + N_MESSAGES_TO_KEEP,
373-
)
387+
const summaryContent: Anthropic.Messages.ContentBlockParam[] = [
388+
summaryTextBlock,
389+
...(slashCommandBlock ? [slashCommandBlock] : []),
390+
...reminderBlocks,
391+
]
374392

375393
// Generate a unique condenseId for this summary
376394
const condenseId = crypto.randomUUID()

src/core/mentions/__tests__/processUserContentMentions.spec.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,4 +335,121 @@ describe("processUserContentMentions", () => {
335335
)
336336
})
337337
})
338+
339+
describe("slash command content processing", () => {
340+
it("should separate slash command content into a new block", async () => {
341+
vi.mocked(parseMentions).mockResolvedValueOnce({
342+
text: "parsed text",
343+
slashCommandHelp: "command help",
344+
mode: undefined,
345+
})
346+
347+
const userContent = [
348+
{
349+
type: "text" as const,
350+
text: "<user_message>Run command</user_message>",
351+
},
352+
]
353+
354+
const result = await processUserContentMentions({
355+
userContent,
356+
cwd: "/test",
357+
urlContentFetcher: mockUrlContentFetcher,
358+
fileContextTracker: mockFileContextTracker,
359+
})
360+
361+
expect(result.content).toHaveLength(2)
362+
expect(result.content[0]).toEqual({
363+
type: "text",
364+
text: "parsed text",
365+
})
366+
expect(result.content[1]).toEqual({
367+
type: "text",
368+
text: "command help",
369+
})
370+
})
371+
372+
it("should include slash command content in tool_result string content", async () => {
373+
vi.mocked(parseMentions).mockResolvedValueOnce({
374+
text: "parsed tool output",
375+
slashCommandHelp: "command help",
376+
mode: undefined,
377+
})
378+
379+
const userContent = [
380+
{
381+
type: "tool_result" as const,
382+
tool_use_id: "123",
383+
content: "<user_message>Tool output</user_message>",
384+
},
385+
]
386+
387+
const result = await processUserContentMentions({
388+
userContent,
389+
cwd: "/test",
390+
urlContentFetcher: mockUrlContentFetcher,
391+
fileContextTracker: mockFileContextTracker,
392+
})
393+
394+
expect(result.content).toHaveLength(1)
395+
expect(result.content[0]).toEqual({
396+
type: "tool_result",
397+
tool_use_id: "123",
398+
content: [
399+
{
400+
type: "text",
401+
text: "parsed tool output",
402+
},
403+
{
404+
type: "text",
405+
text: "command help",
406+
},
407+
],
408+
})
409+
})
410+
411+
it("should include slash command content in tool_result array content", async () => {
412+
vi.mocked(parseMentions).mockResolvedValueOnce({
413+
text: "parsed array item",
414+
slashCommandHelp: "command help",
415+
mode: undefined,
416+
})
417+
418+
const userContent = [
419+
{
420+
type: "tool_result" as const,
421+
tool_use_id: "123",
422+
content: [
423+
{
424+
type: "text" as const,
425+
text: "<user_message>Array item</user_message>",
426+
},
427+
],
428+
},
429+
]
430+
431+
const result = await processUserContentMentions({
432+
userContent,
433+
cwd: "/test",
434+
urlContentFetcher: mockUrlContentFetcher,
435+
fileContextTracker: mockFileContextTracker,
436+
})
437+
438+
expect(result.content).toHaveLength(1)
439+
expect(result.content[0]).toEqual({
440+
type: "tool_result",
441+
tool_use_id: "123",
442+
content: [
443+
{
444+
type: "text",
445+
text: "parsed array item",
446+
},
447+
{
448+
type: "text",
449+
text: "command help",
450+
},
451+
],
452+
})
453+
})
454+
})
338455
})

0 commit comments

Comments
 (0)