Fix slash commands in imported session previews (#1502)
This commit is contained in:
@@ -16,6 +16,20 @@ describe("extractUserMessageText", () => {
|
|||||||
expect(extractUserMessageText(content)).toBe("First line\n\nSecond line");
|
expect(extractUserMessageText(content)).toBe("First line\n\nSecond line");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("returns Claude slash command prompts without transcript tags", () => {
|
||||||
|
const content =
|
||||||
|
"<command-message>diagnose</command-message>\n<command-name>/diagnose</command-name>\n<command-args>recently the PR data does not update</command-args>";
|
||||||
|
|
||||||
|
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 =
|
||||||
|
"<command-message>caveman:caveman</command-message>\n<command-name>/caveman:caveman</command-name>";
|
||||||
|
|
||||||
|
expect(extractUserMessageText(content)).toBe("/caveman:caveman");
|
||||||
|
});
|
||||||
|
|
||||||
test("returns null when no textual content is present", () => {
|
test("returns null when no textual content is present", () => {
|
||||||
const content = [
|
const content = [
|
||||||
{ type: "image", source: "foo.png" },
|
{ type: "image", source: "foo.png" },
|
||||||
|
|||||||
@@ -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:
|
||||||
|
"<command-message>caveman:caveman</command-message>\n<command-name>/caveman:caveman</command-name>",
|
||||||
|
},
|
||||||
|
cwd,
|
||||||
|
sessionId: commandSessionId,
|
||||||
|
})}\n`,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
await fs.writeFile(
|
||||||
|
argsSessionFile,
|
||||||
|
`${JSON.stringify({
|
||||||
|
parentUuid: null,
|
||||||
|
isSidechain: false,
|
||||||
|
type: "user",
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
content:
|
||||||
|
"<command-message>diagnose</command-message>\n<command-name>/diagnose</command-name>\n<command-args>recently the PR data does not update</command-args>",
|
||||||
|
},
|
||||||
|
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", () => {
|
describe("ClaudeAgentSession context window usage", () => {
|
||||||
const logger = createTestLogger();
|
const logger = createTestLogger();
|
||||||
|
|
||||||
|
|||||||
@@ -604,6 +604,9 @@ function isClaudeNoResponsePlaceholderText(value: unknown): boolean {
|
|||||||
|
|
||||||
const LOCAL_COMMAND_STDOUT_PATTERN =
|
const LOCAL_COMMAND_STDOUT_PATTERN =
|
||||||
/^\s*<local-command-stdout>[\s\S]*<\/local-command-stdout>\s*$/;
|
/^\s*<local-command-stdout>[\s\S]*<\/local-command-stdout>\s*$/;
|
||||||
|
const CLAUDE_COMMAND_MESSAGE_PATTERN = /<command-message>([\s\S]*?)<\/command-message>/;
|
||||||
|
const CLAUDE_COMMAND_ARGS_PATTERN = /<command-args>([\s\S]*?)<\/command-args>/;
|
||||||
|
const CLAUDE_COMMAND_NAME_PATTERN = /<command-name>([\s\S]*?)<\/command-name>/;
|
||||||
|
|
||||||
function isClaudeLocalCommandStdout(value: unknown): boolean {
|
function isClaudeLocalCommandStdout(value: unknown): boolean {
|
||||||
const normalized = normalizeClaudeTranscriptText(value);
|
const normalized = normalizeClaudeTranscriptText(value);
|
||||||
@@ -659,7 +662,7 @@ export function extractUserMessageText(content: unknown): string | null {
|
|||||||
if (!normalized || isClaudeTranscriptNoiseText(normalized)) {
|
if (!normalized || isClaudeTranscriptNoiseText(normalized)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return normalized;
|
return normalizeClaudeUserPromptText(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(content)) {
|
if (!Array.isArray(content)) {
|
||||||
@@ -675,7 +678,10 @@ export function extractUserMessageText(content: unknown): string | null {
|
|||||||
if (text && text.trim()) {
|
if (text && text.trim()) {
|
||||||
const trimmed = text.trim();
|
const trimmed = text.trim();
|
||||||
if (!isClaudeTranscriptNoiseText(trimmed)) {
|
if (!isClaudeTranscriptNoiseText(trimmed)) {
|
||||||
parts.push(trimmed);
|
const normalized = normalizeClaudeUserPromptText(trimmed);
|
||||||
|
if (normalized) {
|
||||||
|
parts.push(normalized);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -683,7 +689,10 @@ export function extractUserMessageText(content: unknown): string | null {
|
|||||||
if (input && input.trim()) {
|
if (input && input.trim()) {
|
||||||
const trimmed = input.trim();
|
const trimmed = input.trim();
|
||||||
if (!isClaudeTranscriptNoiseText(trimmed)) {
|
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;
|
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 {
|
function extractClaudeUserText(messageRaw: unknown): string | null {
|
||||||
const message = toObjectRecord(messageRaw);
|
const message = toObjectRecord(messageRaw);
|
||||||
if (!message) {
|
if (!message) {
|
||||||
@@ -5089,11 +5130,13 @@ function extractClaudeUserText(messageRaw: unknown): string | null {
|
|||||||
}
|
}
|
||||||
if (typeof message.content === "string") {
|
if (typeof message.content === "string") {
|
||||||
const normalized = message.content.trim();
|
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") {
|
if (typeof message.text === "string") {
|
||||||
const normalized = message.text.trim();
|
const normalized = message.text.trim();
|
||||||
return normalized && !isClaudeTranscriptNoiseText(normalized) ? normalized : null;
|
if (!normalized || isClaudeTranscriptNoiseText(normalized)) return null;
|
||||||
|
return normalizeClaudeUserPromptText(normalized);
|
||||||
}
|
}
|
||||||
if (isUnknownArray(message.content)) {
|
if (isUnknownArray(message.content)) {
|
||||||
for (const block of message.content) {
|
for (const block of message.content) {
|
||||||
@@ -5101,7 +5144,7 @@ function extractClaudeUserText(messageRaw: unknown): string | null {
|
|||||||
if (blockRecord && typeof blockRecord.text === "string") {
|
if (blockRecord && typeof blockRecord.text === "string") {
|
||||||
const normalized = blockRecord.text.trim();
|
const normalized = blockRecord.text.trim();
|
||||||
if (normalized && !isClaudeTranscriptNoiseText(normalized)) {
|
if (normalized && !isClaudeTranscriptNoiseText(normalized)) {
|
||||||
return normalized;
|
return normalizeClaudeUserPromptText(normalized);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user