diff --git a/packages/server/src/server/agent/providers/claude/agent.history.test.ts b/packages/server/src/server/agent/providers/claude/agent.history.test.ts index 89df8aeb1..2425dcb12 100644 --- a/packages/server/src/server/agent/providers/claude/agent.history.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.history.test.ts @@ -16,6 +16,20 @@ describe("extractUserMessageText", () => { expect(extractUserMessageText(content)).toBe("First line\n\nSecond line"); }); + test("returns Claude slash command prompts without transcript tags", () => { + const content = + "diagnose\n/diagnose\nrecently the PR data does not update"; + + expect(extractUserMessageText(content)).toBe("/diagnose recently the PR data does not update"); + }); + + test("returns the Claude slash command prompt when no args were recorded", () => { + const content = + "caveman:caveman\n/caveman:caveman"; + + expect(extractUserMessageText(content)).toBe("/caveman:caveman"); + }); + test("returns null when no textual content is present", () => { const content = [ { type: "image", source: "foo.png" }, diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index 262999109..6005a1d7c 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -860,6 +860,98 @@ describe("normalizeClaudeAskUserQuestionUpdatedInput", () => { }); }); +describe("ClaudeAgentClient.listImportableSessions", () => { + test("shows Claude slash command prompts without transcript tags", async () => { + const tmpConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-import-")); + const previousConfigDir = process.env.CLAUDE_CONFIG_DIR; + process.env.CLAUDE_CONFIG_DIR = tmpConfigDir; + + try { + const commandSessionId = "session-command-import"; + const argsSessionId = "session-command-args-import"; + const cwd = "/tmp/paseo-test-claude-import"; + const sanitized = cwd.replace(/[\\/._:]/g, "-"); + const projectDir = path.join(tmpConfigDir, "projects", sanitized); + await fs.mkdir(projectDir, { recursive: true }); + const commandSessionFile = path.join(projectDir, `${commandSessionId}.jsonl`); + const argsSessionFile = path.join(projectDir, `${argsSessionId}.jsonl`); + await fs.writeFile( + commandSessionFile, + `${JSON.stringify({ + parentUuid: null, + isSidechain: false, + type: "user", + message: { + role: "user", + content: + "caveman:caveman\n/caveman:caveman", + }, + cwd, + sessionId: commandSessionId, + })}\n`, + "utf-8", + ); + await fs.writeFile( + argsSessionFile, + `${JSON.stringify({ + parentUuid: null, + isSidechain: false, + type: "user", + message: { + role: "user", + content: + "diagnose\n/diagnose\nrecently the PR data does not update", + }, + cwd, + sessionId: argsSessionId, + })}\n`, + "utf-8", + ); + await fs.utimes( + commandSessionFile, + new Date("2026-06-12T10:00:00.000Z"), + new Date("2026-06-12T10:00:00.000Z"), + ); + await fs.utimes( + argsSessionFile, + new Date("2026-06-12T11:00:00.000Z"), + new Date("2026-06-12T11:00:00.000Z"), + ); + + const client = new ClaudeAgentClient({ + logger: createTestLogger(), + resolveBinary: async () => "/test/claude/bin", + }); + + await expect(client.listImportableSessions({ limit: 2 })).resolves.toEqual([ + { + providerHandleId: argsSessionId, + cwd, + title: "/diagnose recently the PR data does not update", + firstPromptPreview: "/diagnose recently the PR data does not update", + lastPromptPreview: "/diagnose recently the PR data does not update", + lastActivityAt: new Date("2026-06-12T11:00:00.000Z"), + }, + { + providerHandleId: commandSessionId, + cwd, + title: "/caveman:caveman", + firstPromptPreview: "/caveman:caveman", + lastPromptPreview: "/caveman:caveman", + lastActivityAt: new Date("2026-06-12T10:00:00.000Z"), + }, + ]); + } finally { + if (previousConfigDir === undefined) { + delete process.env.CLAUDE_CONFIG_DIR; + } else { + process.env.CLAUDE_CONFIG_DIR = previousConfigDir; + } + await fs.rm(tmpConfigDir, { recursive: true, force: true }); + } + }); +}); + describe("ClaudeAgentSession context window usage", () => { const logger = createTestLogger(); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 307f33094..8a0d3d319 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -604,6 +604,9 @@ function isClaudeNoResponsePlaceholderText(value: unknown): boolean { const LOCAL_COMMAND_STDOUT_PATTERN = /^\s*[\s\S]*<\/local-command-stdout>\s*$/; +const CLAUDE_COMMAND_MESSAGE_PATTERN = /([\s\S]*?)<\/command-message>/; +const CLAUDE_COMMAND_ARGS_PATTERN = /([\s\S]*?)<\/command-args>/; +const CLAUDE_COMMAND_NAME_PATTERN = /([\s\S]*?)<\/command-name>/; function isClaudeLocalCommandStdout(value: unknown): boolean { const normalized = normalizeClaudeTranscriptText(value); @@ -659,7 +662,7 @@ export function extractUserMessageText(content: unknown): string | null { if (!normalized || isClaudeTranscriptNoiseText(normalized)) { return null; } - return normalized; + return normalizeClaudeUserPromptText(normalized); } if (!Array.isArray(content)) { @@ -675,7 +678,10 @@ export function extractUserMessageText(content: unknown): string | null { if (text && text.trim()) { const trimmed = text.trim(); if (!isClaudeTranscriptNoiseText(trimmed)) { - parts.push(trimmed); + const normalized = normalizeClaudeUserPromptText(trimmed); + if (normalized) { + parts.push(normalized); + } } continue; } @@ -683,7 +689,10 @@ export function extractUserMessageText(content: unknown): string | null { if (input && input.trim()) { const trimmed = input.trim(); if (!isClaudeTranscriptNoiseText(trimmed)) { - parts.push(trimmed); + const normalized = normalizeClaudeUserPromptText(trimmed); + if (normalized) { + parts.push(normalized); + } } } } @@ -5082,6 +5091,38 @@ function normalizeImportablePromptPreview(text: string): string | null { return normalized.length > 160 ? normalized.slice(0, 160) : normalized; } +function normalizeClaudeUserPromptText(text: string): string | null { + const normalized = text.trim(); + if (!CLAUDE_COMMAND_MESSAGE_PATTERN.test(normalized)) { + return normalized || null; + } + + const command = readClaudeCommandPromptName(normalized); + if (!command) { + return null; + } + + const commandArgs = normalized.match(CLAUDE_COMMAND_ARGS_PATTERN)?.[1]?.trim(); + if (commandArgs) { + return `${command} ${commandArgs}`; + } + + return command; +} + +function readClaudeCommandPromptName(text: string): string | null { + const commandName = text.match(CLAUDE_COMMAND_NAME_PATTERN)?.[1]?.trim(); + if (commandName) { + return commandName.startsWith("/") ? commandName : `/${commandName}`; + } + + const commandMessage = text.match(CLAUDE_COMMAND_MESSAGE_PATTERN)?.[1]?.trim(); + if (!commandMessage) { + return null; + } + return commandMessage.startsWith("/") ? commandMessage : `/${commandMessage}`; +} + function extractClaudeUserText(messageRaw: unknown): string | null { const message = toObjectRecord(messageRaw); if (!message) { @@ -5089,11 +5130,13 @@ function extractClaudeUserText(messageRaw: unknown): string | null { } if (typeof message.content === "string") { const normalized = message.content.trim(); - return normalized && !isClaudeTranscriptNoiseText(normalized) ? normalized : null; + if (!normalized || isClaudeTranscriptNoiseText(normalized)) return null; + return normalizeClaudeUserPromptText(normalized); } if (typeof message.text === "string") { const normalized = message.text.trim(); - return normalized && !isClaudeTranscriptNoiseText(normalized) ? normalized : null; + if (!normalized || isClaudeTranscriptNoiseText(normalized)) return null; + return normalizeClaudeUserPromptText(normalized); } if (isUnknownArray(message.content)) { for (const block of message.content) { @@ -5101,7 +5144,7 @@ function extractClaudeUserText(messageRaw: unknown): string | null { if (blockRecord && typeof blockRecord.text === "string") { const normalized = blockRecord.text.trim(); if (normalized && !isClaudeTranscriptNoiseText(normalized)) { - return normalized; + return normalizeClaudeUserPromptText(normalized); } } }