export type ResponseShellSections = { result: string; why: string; nextAction: string; rest: string; hasShell: boolean; }; type HeadingKind = "result" | "why" | "nextAction"; const SHELL_HEADING_RE = /^\s*\*\*(Result|Why|Next action|Action)\*\*\s*$/gim; function headingKind(raw: string): HeadingKind | null { const low = String(raw || "").trim().toLowerCase(); if (low === "result") return "result"; if (low === "why") return "why"; if (low === "next action" || low === "action") return "nextAction"; return null; } export function splitResponseShellMarkdown(content: string): ResponseShellSections { const text = String(content || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n"); const matches: Array<{ kind: HeadingKind; start: number; bodyStart: number }> = []; let match: RegExpExecArray | null = null; while ((match = SHELL_HEADING_RE.exec(text)) !== null) { const kind = headingKind(match[1]); if (!kind) continue; matches.push({ kind, start: match.index, bodyStart: SHELL_HEADING_RE.lastIndex }); } if (!matches.length) { return { result: text.trim(), why: "", nextAction: "", rest: "", hasShell: false, }; } const buckets: Record = { result: "", why: "", nextAction: "", }; const consumed: Array<[number, number]> = []; for (let i = 0; i < matches.length; i += 1) { const curr = matches[i]; const next = matches[i + 1]; const end = next ? next.start : text.length; const body = text.slice(curr.bodyStart, end).trim(); if (body && !buckets[curr.kind]) buckets[curr.kind] = body; consumed.push([curr.start, end]); } let rest = ""; let cursor = 0; for (const [start, end] of consumed.sort((a, b) => a[0] - b[0])) { if (start > cursor) rest += text.slice(cursor, start); cursor = Math.max(cursor, end); } if (cursor < text.length) rest += text.slice(cursor); return { result: buckets.result.trim(), why: buckets.why.trim(), nextAction: buckets.nextAction.trim(), rest: rest.trim(), hasShell: Boolean(buckets.result || buckets.why || buckets.nextAction), }; }