/**
 * Prompt cleaning and topic extraction for session messages.
 *
 * Strips noise injected by the agent framework (system context lines, XML
 * wrapper tags, team-spawn boilerplate) so that only the real user intent
 * remains. Used by the session picker to show a human-readable topic line.
 */
/** Patterns that cause the entire message to be skipped for topic extraction. */
const WHOLE_MESSAGE_SKIP_PATTERNS = [
    /<permissions instructions>/i,
    /<collaboration_mode>/i,
    /^# AGENTS\.md instructions for\b/im,
    /<local-command-caveat>/i,
    // Slash-command invocations (e.g. /continue, /done) — actual user intent
    // lives in the next user message, not in these wrapper tags.
    /<command-(message|name|args)>/i,
];
/** Per-line noise patterns stripped during prompt cleaning (env context, bare paths, etc). */
const NOISE_LINE_PATTERNS = [
    /^(cwd|shell|current_date|timezone|os|platform|arch|home|user)\b\s*:/i,
    /^\/[\w/.-]+$/,
    /^(bash|zsh|fish|sh|dash)$/i,
    /^\d{4}-\d{2}-\d{2}$/,
    /^[A-Z][A-Za-z]+\/[A-Z][\w+-]+$/,
    /^Caveat:/i,
];
/** XML tag prefix used by local-command messages. */
const LOCAL_COMMAND_PREFIX = '<local-command-caveat>';
/**
 * Patterns that mark a whole `role=user` message as harness-injected scaffolding
 * rather than a genuine user turn: the `!`-prefix shell echo and its output,
 * slash-command wrappers, injected reminders / task-notifications, the local
 * caveat, the interrupt marker, skill bodies, and hook feedback. This is the
 * single cross-harness list — `isSyntheticUserMessage` is consumed by the parser
 * (`parseSession`) to flag such events `_synthetic`, so `--include user` and the
 * `--first`/`--last` turn split see only real intent, and every consumer (the
 * session picker, `session-recall`, external hooks) gets clean user turns without
 * re-implementing the stripping. Extend this list, not each caller.
 */
const SYNTHETIC_USER_MESSAGE_PATTERNS = [
    /^\s*<\/?(?:bash-input|bash-stdout|bash-stderr)>/i,
    /^\s*<\/?(?:command-name|command-message|command-args|command-contents)>/i,
    /^\s*<\/?(?:local-command-stdout|local-command-stderr|local-command-caveat)>/i,
    /^\s*<\/?system-reminder>/i,
    /^\s*<\/?task-notification>/i,
    /^\s*<\/?user-prompt-submit-hook>/i,
    /^\s*<\/?persisted-output>/i,
    /^\s*<permissions instructions>/i,
    /^\s*<(?:apps|plugins|skills)_instructions>/i,
    /^\s*<recommended_plugins>/i,
    /^\s*<(?:multi_agent_mode|environment_context)>/i,
    /^\s*<user_info>/i,
    /^\s*## (?:In-flight in this repo|Host & Fleet)/i,
    /^\s*Your current session id is\b/i,
    /^\s*Linear context skipped:/i,
    /^\s*<collaboration_mode>/i,
    /^\s*Caveat: The messages below were generated by the user while running/i,
    /^\s*\[Request interrupted/i,
    /^\s*Base directory for this skill:/i,
    /^\s*[A-Za-z][A-Za-z-]* hook feedback:/,
];
/**
 * True when a `role=user` message is harness-injected scaffolding, not a genuine
 * user turn. Text-based so it is cross-harness by construction (the markers are
 * the same wrappers across Claude/Codex/etc.); harnesses that already drop their
 * own scaffolding at parse time simply never reach here.
 */
export function isSyntheticUserMessage(raw) {
    if (!raw)
        return false;
    return SYNTHETIC_USER_MESSAGE_PATTERNS.some(pattern => pattern.test(raw));
}
/**
 * Inner text of a `<user_query>` wrapper, or the original string if none.
 * Same extraction Cursor already applies in `parseCursorUserText`.
 */
export function unwrapUserQuery(text) {
    const query = text.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/);
    return (query?.[1] ?? text).trim();
}
/**
 * Return one genuine user turn in full.
 * Synthetic scaffolding is rejected as a whole so an injected Apps/AGENTS
 * preamble can never become the session's displayed request. Unlike the topic
 * cleaner, this intentionally preserves internal whitespace and every line of
 * the user's actual request. A Grok/Cursor `<user_query>` wrapper is unwrapped
 * so the stored turn is the originating request, not the harness tags.
 */
export function cleanFirstUserMessage(raw) {
    if (!raw || isSyntheticUserMessage(raw))
        return undefined;
    const clean = unwrapUserQuery(raw);
    return clean || undefined;
}
/** First genuine user message from an already-normalized event stream. */
export function firstUserMessageFromEvents(events) {
    for (const event of events) {
        if (event.type !== 'message' || event.role !== 'user' || event._synthetic)
            continue;
        const first = cleanFirstUserMessage(event.content);
        if (first)
            return first;
    }
    return undefined;
}
/**
 * The `<command-name>` wrapper Claude injects into a `role=user` message when
 * a typed slash command is invoked, e.g.:
 *   <command-message>recap</command-message>
 *   <command-name>/recap</command-name>
 * Captures the name INCLUDING its leading slash (`/recap`), matching how the
 * `SlashCommand` tool's own `command` input is formatted (see
 * `extractSlashCommandFromToolInput`) so both sources land on the same shape.
 */
const COMMAND_NAME_WRAPPER_RE = /<command-name>\s*([^<]*?)\s*<\/command-name>/i;
/**
 * Extract the invoked slash-command name from a raw `role=user` message, or
 * undefined when the message carries no `<command-name>` wrapper. Used by
 * `parseClaudeContent` to populate `SessionEvent.slashCommand`.
 *
 * Normalizes a leading `/` onto the result: current Claude Code wraps the
 * name WITH the slash already (`/recap`), but the wrapper predates that and
 * older transcripts carry the bare name (`recap`) — normalizing here means
 * every consumer sees one consistent `/name` shape regardless of vintage.
 */
export function extractSlashCommandName(raw) {
    if (!raw)
        return undefined;
    const m = raw.match(COMMAND_NAME_WRAPPER_RE);
    const name = m?.[1]?.trim();
    if (!name)
        return undefined;
    return name.startsWith('/') ? name : `/${name}`;
}
/**
 * Extract the invoked slash-command name from the `SlashCommand` tool's input
 * — the MODEL invoking a command programmatically, not the user typing it.
 * The tool's `command` field carries the full invocation text (name plus any
 * args, e.g. `/code:commit fix the bug`); only the leading command token is
 * the name, matching {@link extractSlashCommandName}'s shape.
 */
export function extractSlashCommandFromToolInput(toolInput) {
    const command = toolInput?.command;
    if (typeof command !== 'string')
        return undefined;
    const name = command.trim().split(/\s+/)[0];
    return name || undefined;
}
// Prefix prepended to every Claude-in-plan-mode team spawn prompt.
// Ends at a blank line before the real user task.
export const HEADLESS_PLAN_MODE_PREFIX = 'You are running in HEADLESS PLAN MODE.';
// Wrappers added to team-spawned prompts in src/lib/teams/agents.ts.
// Stripped before topic extraction so the picker shows what the user actually typed.
/** Marker string appended to team-spawned prompts; stripped before topic extraction. */
const TEAM_PROMPT_SUFFIX_MARKER = "When you're done, provide a brief summary of:";
/** Remove headless-plan-mode prefix and team summary suffix from a raw prompt. */
function stripTeamWrappers(raw) {
    let text = raw;
    if (text.trimStart().startsWith(HEADLESS_PLAN_MODE_PREFIX)) {
        const blankLine = text.indexOf('\n\n');
        if (blankLine === -1)
            return '';
        text = text.slice(blankLine + 2);
    }
    const suffixIdx = text.indexOf(TEAM_PROMPT_SUFFIX_MARKER);
    if (suffixIdx !== -1)
        text = text.slice(0, suffixIdx);
    return text.trim();
}
/** Strip framework noise from a raw session prompt, returning only meaningful user text. */
export function cleanSessionPrompt(raw) {
    let text = stripTeamWrappers(raw).replace(/\r/g, '').trim();
    if (!text)
        return '';
    text = text.replace(/<\/?[a-z_][a-z0-9_-]*>/gi, '');
    const meaningful = text
        .split('\n')
        .map(line => line.trim())
        .filter(Boolean)
        .filter(line => !NOISE_LINE_PATTERNS.some(pattern => pattern.test(line)));
    return meaningful.join('\n').trim();
}
/**
 * A skill invocation injects a system line naming the skill's install directory:
 *   "Base directory for this skill: /home/.../skills/<name>"
 * which collapses to "/<name>" so a row shows the skill, not the path. Anchored
 * to that exact injected line — a bare `.../skills/<name>` mention in ordinary
 * prose is NOT a skill invocation and must keep its real text.
 */
const SKILL_BASEDIR_RE = /Base directory for this skill:\s*\S*[\/\\]skills[\/\\]([\w.-]+)/i;
/**
 * An explicit shell-prompt paste (`$ cmd`). Only the command survives. Deliberately
 * `$`-only: a leading `>` is a markdown blockquote far more often than a shell
 * prompt, so matching it misread quoted prose as a command.
 */
const COMMAND_PREFIX_RE = /^\$\s+(\S.*)$/;
/**
 * A filesystem path (absolute or `~`-rooted) ending in an image extension —
 * matched even across the spaces a CleanShot filename carries. Used both to
 * detect an image-only prompt and to drop the path from a mixed one.
 */
const IMAGE_PATH_RE = /[\/~][^\n]*?\.(?:png|jpe?g|gif|webp|heic|bmp|svg)\b/i;
/**
 * Classify a raw first user turn into a display-ready `{ clean, kind }`, so a
 * Fleet/session row shows the user's actual intent rather than path noise:
 *
 *   - `skill`   — a `/skill` invocation's injected "Base directory for this
 *                 skill: …" line collapses to `/<name>`.
 *   - `command` — a `$ `-prefixed shell paste keeps only the first command.
 *   - `image`   — a screenshot/image path standing alone, or an image
 *                 attachment with no real accompanying text, becomes `[image]`.
 *   - `text`    — everything else: wrapper tags stripped (via
 *                 {@link cleanSessionPrompt}) and any inline image path replaced
 *                 with `[image]` so a mixed prompt still drops the path.
 *
 * Pure and cross-harness (text markers, no per-agent branch). `hasImageAttachment`
 * lets a caller that only has an already-extracted topic line still detect an
 * image-only turn from the row's attachment list.
 */
export function classifyUserPrompt(raw, opts = {}) {
    const text = (raw ?? '').replace(/\r/g, '').trim();
    const skill = text.match(SKILL_BASEDIR_RE);
    if (skill)
        return { clean: `/${skill[1]}`, kind: 'skill' };
    const firstLine = text.split('\n').map(line => line.trim()).find(Boolean) ?? '';
    const cmd = firstLine.match(COMMAND_PREFIX_RE);
    if (cmd)
        return { clean: `$ ${cmd[1]}`.trim(), kind: 'command' };
    const cleaned = cleanSessionPrompt(text);
    const cleanedFirst = cleaned.split('\n').map(line => line.trim()).find(Boolean) ?? '';
    const imgMatch = firstLine.match(IMAGE_PATH_RE);
    const imageOnly = !!imgMatch && firstLine.replace(imgMatch[0], '').replace(/[^\w]/g, '').length <= 8;
    if (imageOnly || (opts.hasImageAttachment && !cleanedFirst)) {
        return { clean: '[image]', kind: 'image' };
    }
    const display = (cleanedFirst || firstLine).replace(IMAGE_PATH_RE, '[image]').trim();
    return { clean: display, kind: 'text' };
}
/**
 * Collapse a harness-generated session title when it is injected skill
 * scaffolding; otherwise leave the title as the harness wrote it.
 *
 * Claude's `ai-title` and Cursor's `chatMeta.title` are both derived from the
 * first turn, so a session opened with a skill gets named after the injected
 * "Base directory for this skill: …" line. {@link classifyUserPrompt} reports
 * `kind: 'skill'` only for that line, so collapse it to `/<skill>`. Empty or
 * whitespace-only input yields `undefined` so the caller falls through to the
 * first-prompt topic.
 *
 * A user-authored title (Claude `/rename` / `custom-title`) is never passed
 * here — the caller keeps it verbatim.
 */
export function cleanGeneratedSessionLabel(title) {
    const trimmed = title?.trim();
    if (!trimmed)
        return undefined;
    const classified = classifyUserPrompt(trimmed);
    return classified.kind === 'skill' ? classified.clean : trimmed;
}
/** Extract a one-line topic from a raw user message, or undefined if the message is pure noise. */
export function extractSessionTopic(raw) {
    if (!raw.trim())
        return undefined;
    if (WHOLE_MESSAGE_SKIP_PATTERNS.some(pattern => pattern.test(raw))) {
        return undefined;
    }
    const cleaned = cleanSessionPrompt(raw);
    if (!cleaned)
        return undefined;
    if (WHOLE_MESSAGE_SKIP_PATTERNS.some(pattern => pattern.test(cleaned))) {
        return undefined;
    }
    const firstLine = cleaned.split('\n').map(line => line.trim()).find(Boolean);
    return firstLine || undefined;
}
