154 lines
4.9 KiB
TypeScript
154 lines
4.9 KiB
TypeScript
/**
|
|
* Slash Command Extractor - Handle slash command extraction from AI Group responses
|
|
*
|
|
* Extracts and processes slash command invocations and their follow-up instructions.
|
|
*/
|
|
|
|
import { extractSlashInfo, isCommandContent } from '@shared/utils/contentSanitizer';
|
|
|
|
import { estimateTokens, toDate } from './aiGroupHelpers';
|
|
|
|
import type { ParsedMessage } from '@renderer/types/data';
|
|
import type { SlashItem } from '@renderer/types/groups';
|
|
|
|
/**
|
|
* Info about the preceding user message's slash invocation.
|
|
* This is passed from the UserGroup to help link slash outputs to slash names.
|
|
*/
|
|
export interface PrecedingSlashInfo {
|
|
/** Slash name (e.g., "claude-hud:setup", "isolate-context", "model") */
|
|
name: string;
|
|
/** Message content from <command-message> */
|
|
message?: string;
|
|
/** Arguments from <command-args> */
|
|
args?: string;
|
|
/** UUID of the slash command message */
|
|
commandMessageUuid: string;
|
|
/** Timestamp of the slash command message */
|
|
timestamp: Date;
|
|
}
|
|
|
|
/**
|
|
* Extract slash items from AI group responses.
|
|
*
|
|
* All slash invocations follow the same format:
|
|
* <command-name>/xxx</command-name>
|
|
* <command-message>xxx</command-message>
|
|
* <command-args>optional</command-args>
|
|
*
|
|
* Strategy:
|
|
* 1. Build a map of follow-up messages (isMeta:true with parentUuid) by their parentUuid
|
|
* 2. If precedingSlash is provided, create a SlashItem with its follow-up instructions
|
|
* 3. Also check for any slash invocations in responses (fallback)
|
|
*
|
|
* @param responses - All response messages in the AI group
|
|
* @param precedingSlash - Optional slash info from the preceding UserGroup
|
|
* @returns Array of SlashItem objects ready for display
|
|
*/
|
|
export function extractSlashes(
|
|
responses: ParsedMessage[],
|
|
precedingSlash?: PrecedingSlashInfo
|
|
): SlashItem[] {
|
|
const slashes: SlashItem[] = [];
|
|
|
|
// Build a map of follow-up messages by their parentUuid
|
|
// These are isMeta:true messages that contain slash instructions/output
|
|
const followUpsByParentUuid = new Map<
|
|
string,
|
|
{
|
|
text: string;
|
|
timestamp: Date;
|
|
}
|
|
>();
|
|
|
|
// Also build a map of potential slash messages from responses (fallback)
|
|
const slashMessagesById = new Map<
|
|
string,
|
|
{
|
|
uuid: string;
|
|
name: string;
|
|
message?: string;
|
|
args?: string;
|
|
timestamp: Date;
|
|
}
|
|
>();
|
|
|
|
for (const msg of responses) {
|
|
// Look for slash messages (user messages with string content containing <command-name>)
|
|
// This is a fallback in case the slash invocation is somehow in responses
|
|
if (msg.type === 'user' && typeof msg.content === 'string' && isCommandContent(msg.content)) {
|
|
const slashInfo = extractSlashInfo(msg.content);
|
|
if (slashInfo) {
|
|
slashMessagesById.set(msg.uuid, {
|
|
uuid: msg.uuid,
|
|
name: slashInfo.name,
|
|
message: slashInfo.message,
|
|
args: slashInfo.args,
|
|
timestamp: toDate(msg.timestamp),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Look for follow-up isMeta messages with parentUuid
|
|
if (
|
|
msg.type === 'user' &&
|
|
msg.isMeta === true &&
|
|
msg.parentUuid &&
|
|
!msg.sourceToolUseID && // Exclude tool-call related messages
|
|
Array.isArray(msg.content)
|
|
) {
|
|
// Extract text from the message
|
|
for (const block of msg.content) {
|
|
if (block.type === 'text' && block.text) {
|
|
const text = block.text;
|
|
followUpsByParentUuid.set(msg.parentUuid, {
|
|
text,
|
|
timestamp: toDate(msg.timestamp),
|
|
});
|
|
break; // Only need the first text block
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Strategy 1: If we have precedingSlash info, create a SlashItem with its follow-up
|
|
if (precedingSlash) {
|
|
const followUp = followUpsByParentUuid.get(precedingSlash.commandMessageUuid);
|
|
|
|
slashes.push({
|
|
id: `slash-${precedingSlash.commandMessageUuid}`,
|
|
name: precedingSlash.name,
|
|
message: precedingSlash.message,
|
|
args: precedingSlash.args,
|
|
commandMessageUuid: precedingSlash.commandMessageUuid,
|
|
instructions: followUp?.text,
|
|
instructionsTokenCount: followUp ? estimateTokens(followUp.text) : undefined,
|
|
// Use follow-up timestamp if available (sorts with other AI items),
|
|
// otherwise fall back to slash invocation timestamp
|
|
timestamp: followUp?.timestamp ?? precedingSlash.timestamp,
|
|
});
|
|
}
|
|
|
|
// Strategy 2: Fallback - match slash messages found in responses to their follow-ups
|
|
for (const [uuid, slashMsg] of slashMessagesById.entries()) {
|
|
// Skip if we already added this slash via precedingSlash
|
|
if (uuid === precedingSlash?.commandMessageUuid) {
|
|
continue;
|
|
}
|
|
|
|
const followUp = followUpsByParentUuid.get(uuid);
|
|
|
|
slashes.push({
|
|
id: `slash-${uuid}`,
|
|
name: slashMsg.name,
|
|
message: slashMsg.message,
|
|
args: slashMsg.args,
|
|
commandMessageUuid: uuid,
|
|
instructions: followUp?.text,
|
|
instructionsTokenCount: followUp ? estimateTokens(followUp.text) : undefined,
|
|
timestamp: slashMsg.timestamp,
|
|
});
|
|
}
|
|
|
|
return slashes;
|
|
}
|