import { parseIpythonBashCell } from "./ipython-cell-code.js"; const DESCRIPTOR_MAX_WIDTH = 64; const MAGIC_LINE_PATTERN = /^\s*!/; const COMMENT_LINE_PATTERN = /^\s*#/; const CD_PREFIX_PATTERN = /^\s*cd\s+([^&;|]+)(?:&&|;)\s*/; const BASH_SET_PATTERN = /^\s*set\s+[-+][A-Za-z]*(?:\s+[-+]?\w+)*(?:\s+pipefail)?\s*$/; const BASH_SETUP_PATTERN = /^(?:export\s+\w+=|source\s+\S+|\.\s+\S+)/; const PYTHON_IMPORT_PATTERN = /^\s*(?:import\s+\S|from\s+\S+\s+import\s+)/; const PYTHON_DECORATOR_PATTERN = /^\s*@/; const PYTHON_DEFINITION_PATTERN = /^\s*(?:async\s+def|def|class)\s+/; const PYTHON_MAIN_PATTERN = /^\s*if\s+__name__\s*==\s*['"]__main__['"]\s*:/; const PYTHON_CONTROL_PATTERN = /^\s*(?:if|elif|else|for|while|with|try|except|finally)\b.*:\s*$/; const PYTHON_CALL_PATTERN = /^\s*(?:await\s+)?[A-Za-z_][A-Za-z0-9_.]*\s*\(/; const BASH_SKILL_CALL_PATTERN = /^\s*(?:[A-Za-z_][A-Za-z0-9_]*\s*=\s*)?(?:await\s+)?bash\s*\(\s*[rR]?("""|'''|"|')/; const DYNAMIC_BASH_SKILL_CALL_PATTERN = /^\s*(?:[A-Za-z_][A-Za-z0-9_]*\s*=\s*)?(?:await\s+)?bash\s*\(/; const PYTHON_LOW_SIGNAL_CALL_PATTERN = /^\s*(?:await\s+)?(?:print|len|str|repr|int|float|list|dict|set|tuple)\s*\(/; const PYTHON_SLEEP_CALL_PATTERN = /^\s*(?:await\s+)?(?:asyncio\.sleep|time\.sleep|sleep)\s*\(/; const PYTHON_CLEANUP_CALL_PATTERN = /^\s*(?:await\s+)?(?:[A-Za-z0-9_.]*\.)?(?:close|quit|shutdown)\s*\(\s*\)/; const PYTHON_LOW_SIGNAL_STATEMENT_PATTERN = /^\s*(?:break|continue|pass|return(?:\s+None)?)\s*$/; const PYTHON_ASSIGNMENT_CALL_PATTERN = /^\s*[A-Za-z_][A-Za-z0-9_]*(?:\s*:\s*[^=]+)?\s*=\s*(?:await\s+)?[A-Za-z_][A-Za-z0-9_.]*\s*\(/; const PYTHON_LOW_SIGNAL_ASSIGNMENT_CALL_PATTERN = /^\s*[A-Za-z_][A-Za-z0-9_]*(?:\s*:\s*[^=]+)?\s*=\s*(?:await\s+)?(?:Path|pathlib\.Path|json\.loads|json\.dumps|str|int|float|list|dict|set|tuple)\s*\(/; const PYTHON_EFFECT_CALL_PATTERN = /^\s*(?:await\s+)?[A-Za-z_][A-Za-z0-9_.]*\.(?:write_text|write_bytes|mkdir|unlink|rename|replace|touch|append|extend|update|add|remove|discard|commit|execute|run)\s*\(/; const EDIT_SKILL_PATTERN = /^(?:[A-Za-z_][A-Za-z0-9_]*\s*=\s*)?(?:await\s+)?edit\s*\(\s*(?:path\s*=\s*)?["']([^"']+)["']/; const SEARCH_SKILL_PATTERN = /^(?:[A-Za-z_][A-Za-z0-9_]*\s*=\s*)?(?:await\s+)?(?:search|search\.files)\s*\(\s*["']([^"']+)["'](?:\s*,\s*(?:path\s*=\s*)?["']([^"']+)["'])?/; const VCS_SKILL_PATTERN = /^(?:[A-Za-z_][A-Za-z0-9_]*\s*=\s*)?(?:await\s+)?(git|jj)\.([a-z_]+)\s*\(/; const RLM_SUBAGENT_PATTERN = /^(?:[A-Za-z_][A-Za-z0-9_]*\s*=\s*)?(?:await\s+)?rlm\(\s*["']([^"']+)["'](?:\s*,\s*name\s*=\s*["']([^"']+)["'])?/; const INLINE_PATH_FILE_OP_PATTERN = /^(?:[A-Za-z_][A-Za-z0-9_]*\s*=\s*)?(?:await\s+)?(?:Path|pathlib\.Path)\(\s*["']([^"']+)["']\s*\)\.(write_text|write_bytes|read_text|read_bytes|mkdir|unlink|rename|replace|touch)\s*\(/; const INLINE_OPEN_READ_PATTERN = /^(?:[A-Za-z_][A-Za-z0-9_]*\s*=\s*)?(?:await\s+)?open\(\s*["']([^"']+)["'](?:\s*,\s*["']([^"']+)["'])?\s*\)\.(read|readlines|read_text)\s*\(/; const INLINE_OPEN_WRITE_PATTERN = /^(?:[A-Za-z_][A-Za-z0-9_]*\s*=\s*)?(?:await\s+)?open\(\s*["']([^"']+)["']\s*,\s*["']([wa][xb+]*)["']\s*\)\.write\s*\(/; const WITH_OPEN_PATTERN = /^\s*with\s+open\(\s*["']([^"']+)["'](?:\s*,\s*["']([^"']+)["'])?\s*\)/; const HEREDOC_PATTERN = /<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/; const PATH_ASSIGN_PATTERN = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:Path|pathlib\.Path)\(["']([^"']+)["']\)/; const STRING_ASSIGN_PATTERN = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*["']([^"']+)["']/; export type CodePreviewLanguage = "bash" | "python"; export interface CodePreview { language: CodePreviewLanguage; text: string; } interface PreviewCandidate { language: CodePreviewLanguage; text: string; score: number; index: number; } function collapseWhitespace(text: string): string { return text.replace(/\s+/g, " ").trim(); } function truncateDescriptor(text: string): string { if (text.length <= DESCRIPTOR_MAX_WIDTH) { return text; } return `${text.slice(0, DESCRIPTOR_MAX_WIDTH - 1).trimEnd()}…`; } function redactNoise(text: string): string { return text .replace(/[A-Za-z0-9+/]{80,}={0,2}/g, "") .replace(/\b((?=\w*(?:token|key|secret|password))[A-Za-z_]\w*)\s*=\s*(["'])[^"']*\2/gi, "$1=") .replace( /\b((?=\w*(?:token|key|secret|password))[A-Za-z_]\w*)\s*=\s*(?!)(?!["'])\S+/gi, "$1=", ) .replace(/\b(authorization:\s*(?:bearer\s+)?)[^\s"']+/gi, "$1") .replace(/(["'])sk-[^"']+\1/g, "$1$1") .replace(/(["']).{160,}\1/g, "$1…$1"); } function descriptor(text: string): string { return truncateDescriptor(collapseWhitespace(redactNoise(text))); } function stripBashPrefix(line: string): string { return line.replace(MAGIC_LINE_PATTERN, "").trim().replace(CD_PREFIX_PATTERN, "").trim(); } function isSkippableBashLine(line: string): boolean { const trimmed = line.trim(); return ( !trimmed || COMMENT_LINE_PATTERN.test(trimmed) || BASH_SET_PATTERN.test(trimmed) || BASH_SETUP_PATTERN.test(trimmed) ); } function shellWords(line: string): string[] { const words: string[] = []; const pattern = /"([^"]*)"|'([^']*)'|(\S+)/g; for (const match of line.matchAll(pattern)) { words.push(match[1] ?? match[2] ?? match[3] ?? ""); } return words; } function pathTail(path: string): string { return path.replace(/^\.\//, ""); } function simplifyRunnerCommand(line: string): string | undefined { const words = shellWords(line); const joined = words.join(" "); const vitestIndex = words.findIndex((word) => /(?:^|\/)vitest\/dist\/cli\.js$/.test(word)); if (words[0] === "npx" && words[1] === "tsx" && vitestIndex >= 2) { return `vitest ${words.slice(vitestIndex + 1).join(" ")}`.trim(); } if (words[0] === "npm") { const prefixIndex = words.indexOf("--prefix"); const cwd = prefixIndex >= 0 ? words[prefixIndex + 1] : undefined; const runIndex = words.indexOf("run"); if (runIndex >= 0 && words[runIndex + 1]) { const command = `npm ${words[runIndex + 1]} ${words.slice(runIndex + 2).join(" ")}`.trim(); return cwd ? `${command} (${pathTail(cwd)})` : command; } } if (words[0] === "pnpm") { const cwdIndex = words.findIndex((word) => word === "-C" || word === "--dir"); const cwd = cwdIndex >= 0 ? words[cwdIndex + 1] : undefined; const rest = words.filter((_, index) => index !== cwdIndex && index !== cwdIndex + 1); return cwd ? `${rest.join(" ")} (${pathTail(cwd)})` : undefined; } const pytestIndex = words.findIndex( (word, index) => word === "pytest" || (word === "pytest" && words[index - 1] === "-m"), ); if (words[0] === "uv" && words[1] === "run" && pytestIndex >= 0) { return `pytest ${words.slice(pytestIndex + 1).join(" ")}`.trim(); } if ((words[0] === "python" || words[0] === "python3") && words[1] === "-m" && words[2] === "pytest") { return `pytest ${words.slice(3).join(" ")}`.trim(); } if (joined.includes("node_modules/.bin/")) { return joined.replace(/\S*node_modules\/\.bin\//g, ""); } return undefined; } function simplifyMutationCommand(line: string): string | undefined { const words = shellWords(line); if (words.length === 0) return undefined; if (words[0] === "cat" && words[1] === ">" && words[2]) return `write ${pathTail(words[2])}`; if (words[0] === "tee" && words.at(-1)) return `${words.includes("-a") ? "append" : "write"} ${pathTail(words.at(-1) ?? "")}`; if (words[0] === "apply_patch") return "apply patch"; if (["rm", "mv", "cp", "git", "npm"].includes(words[0] ?? "")) return line; if ( (words[0] === "sed" && words.some((word) => word.startsWith("-i"))) || (words[0] === "perl" && words.includes("-pi")) ) { return line; } return undefined; } function simplifyBashCommandLine(line: string): string { return simplifyRunnerCommand(line) ?? simplifyMutationCommand(line) ?? line; } function splitCommandChain(line: string): string[] { return line .split(/\s*(?:&&|;)\s*/) .map((part) => part.trim()) .filter(Boolean); } function heredocBody(lines: readonly string[], startIndex: number, delimiter: string): string | undefined { // While args stream, preview the partial heredoc body rather than the low-signal heredoc opener. const body: string[] = []; for (let i = startIndex + 1; i < lines.length; i++) { const line = lines[i] ?? ""; if (line.trim() === delimiter) { return body.join("\n"); } body.push(line); } return body.length > 0 ? body.join("\n") : undefined; } function previewHeredoc(lines: readonly string[]): CodePreview | undefined { // A generic heredoc body is low-signal; keep it as a fallback and prefer a // later, more specific heredoc (python/bash/node/write) if one follows. let fallback: CodePreview | undefined; for (let i = 0; i < lines.length; i++) { const line = stripBashPrefix(lines[i] ?? ""); if (isSkippableBashLine(line)) { continue; } const heredocMatch = line.match(HEREDOC_PATTERN); const delimiter = heredocMatch?.[1]; if (!delimiter) { continue; } const body = heredocBody(lines, i, delimiter); if (!body) { continue; } if (/\b(?:uv\s+run\s+)?python3?\b/.test(line)) { const preview = previewPythonCode(body); if (preview.text) { return preview; } continue; } // Match bash/sh as an interpreter word (incl. /bin/sh), not a path suffix like script.sh. if (/(?|\s)(\S+)\s*<<-?/); if (catWrite?.[1]) { return { language: "bash", text: `${line.includes("tee -a") ? "append" : "write"} ${pathTail(catWrite[1])}` }; } if (/\bapply_patch\b/.test(line)) { return { language: "bash", text: "apply patch" }; } fallback ??= { language: "bash", text: descriptor(body) }; } return fallback; } function bashLineScore(line: string, index: number): number { const simplified = simplifyBashCommandLine(line); const words = shellWords(line); let score = 30; if (simplified !== line) score += 40; if (["rm", "mv", "cp", "git", "npm", "pnpm", "pytest", "vitest"].includes(words[0] ?? "")) score += 20; if (/\b(?:rm|mv|cp|git\s+(?:add|commit)|npm\s+install|sed\s+-i|perl\s+-pi|tee|cat\s*>|apply_patch)\b/.test(line)) score += 40; return score + index; } export function previewBashCommand(command: string): CodePreview { const lines = command.split("\n"); const heredoc = previewHeredoc(lines); if (heredoc?.text) { return { language: heredoc.language, text: descriptor(heredoc.text) }; } let best: PreviewCandidate | undefined; let index = 0; for (const rawLine of lines) { for (const rawPart of splitCommandChain(rawLine)) { const commandLine = stripBashPrefix(rawPart.trim()); if (!commandLine || isSkippableBashLine(commandLine)) { continue; } const candidate = { language: "bash" as const, text: simplifyBashCommandLine(commandLine), score: bashLineScore(commandLine, index), index, }; if (!best || candidate.score > best.score) { best = candidate; } index += 1; } } return { language: "bash", text: best ? descriptor(best.text) : "" }; } function isSkippablePythonLine(line: string): boolean { const trimmed = line.trim(); return !trimmed || COMMENT_LINE_PATTERN.test(trimmed) || PYTHON_IMPORT_PATTERN.test(trimmed); } function pythonIndent(line: string): number { const match = line.match(/^\s*/); return match?.[0].length ?? 0; } function pythonPrintInnerCall(line: string): string | undefined { const printMatch = line.trim().match(/^print\((.*)\)$/); const inner = printMatch?.[1]?.trim(); if (!inner) return undefined; const callMatch = inner.match(/(?:await\s+)?[A-Za-z_][A-Za-z0-9_.]*\s*\(/); return callMatch && !PYTHON_LOW_SIGNAL_CALL_PATTERN.test(callMatch[0]) ? inner : undefined; } function pythonPathVars(lines: readonly string[]): Map { const vars = new Map(); for (const line of lines) { const pathMatch = line.match(PATH_ASSIGN_PATTERN) ?? line.match(STRING_ASSIGN_PATTERN); if (pathMatch?.[1] && pathMatch[2]?.includes("/")) { vars.set(pathMatch[1], pathMatch[2]); } } return vars; } function pythonFileOperation(line: string, paths: ReadonlyMap): string | undefined { const trimmed = line.trim(); const varMatch = trimmed.match( /^(?:[A-Za-z_][A-Za-z0-9_]*\s*=\s*)?(?:await\s+)?([A-Za-z_][A-Za-z0-9_]*)\.(write_text|write_bytes|read_text|read_bytes|mkdir|unlink|rename|replace|touch)\s*\(/, ); if (varMatch?.[1] && varMatch[2]) { const path = paths.get(varMatch[1]); if (path) { const action: Record = { write_text: "write", write_bytes: "write", read_text: "read", read_bytes: "read", mkdir: "mkdir", unlink: "delete", rename: "rename", replace: "replace", touch: "touch", }; return `${action[varMatch[2]] ?? varMatch[2]} ${pathTail(path)}`; } } const inlinePathMatch = trimmed.match(INLINE_PATH_FILE_OP_PATTERN); if (inlinePathMatch?.[1] && inlinePathMatch[2]) { const action: Record = { write_text: "write", write_bytes: "write", read_text: "read", read_bytes: "read", mkdir: "mkdir", unlink: "delete", rename: "rename", replace: "replace", touch: "touch", }; return `${action[inlinePathMatch[2]] ?? inlinePathMatch[2]} ${pathTail(inlinePathMatch[1])}`; } const inlineOpenRead = trimmed.match(INLINE_OPEN_READ_PATTERN); if (inlineOpenRead?.[1]) { return `read ${pathTail(inlineOpenRead[1])}`; } const inlineOpenWrite = trimmed.match(INLINE_OPEN_WRITE_PATTERN); if (inlineOpenWrite?.[1]) { return `write ${pathTail(inlineOpenWrite[1])}`; } const withOpen = trimmed.match(WITH_OPEN_PATTERN); if (withOpen?.[1]) { const mode = withOpen[2] ?? ""; const verb = mode.includes("w") || mode.includes("a") ? "write" : "read"; return `${verb} ${pathTail(withOpen[1])}`; } return undefined; } function pythonSkillOperation(line: string): string | undefined { const trimmed = line.trim(); const editMatch = trimmed.match(EDIT_SKILL_PATTERN); if (editMatch?.[1]) { return `edit ${pathTail(editMatch[1])}`; } const searchMatch = trimmed.match(SEARCH_SKILL_PATTERN); if (searchMatch?.[1]) { return searchMatch[2] ? `search "${searchMatch[1]}" (${pathTail(searchMatch[2])})` : `search "${searchMatch[1]}"`; } const vcsMatch = trimmed.match(VCS_SKILL_PATTERN); if (vcsMatch?.[1] && vcsMatch[2]) { return `${vcsMatch[1]} ${vcsMatch[2].replace(/_/g, " ")}`; } const rlmMatch = trimmed.match(RLM_SUBAGENT_PATTERN); if (rlmMatch?.[1]) { return rlmMatch[2] ? `subagent ${rlmMatch[2]}` : `subagent "${rlmMatch[1]}"`; } return undefined; } function pythonSubprocessCommand(line: string): string | undefined { const trimmed = line.trim(); const shellMatch = trimmed.match(/subprocess\.(?:run|check_call|check_output|Popen)\(\s*["`]([^"`]+)["`]/); if (shellMatch?.[1]) return simplifyBashCommandLine(shellMatch[1]); const listMatch = trimmed.match(/subprocess\.(?:run|check_call|check_output|Popen)\(\s*\[([^\]]+)\]/); if (listMatch?.[1]) { const words = [...listMatch[1].matchAll(/["']([^"']+)["']/g)].map((match) => match[1] ?? ""); return simplifyBashCommandLine(words.join(" ")); } return undefined; } function simplifyPythonPreviewLine(line: string, paths: ReadonlyMap): string { return ( pythonFileOperation(line, paths) ?? pythonSkillOperation(line) ?? pythonSubprocessCommand(line) ?? pythonPrintInnerCall(line) ?? line.trim() ); } function pythonPreviewLine(lines: readonly string[], index: number, paths: ReadonlyMap): string { const line = lines[index] ?? ""; const fileOp = pythonFileOperation(line, paths); if (fileOp) { return fileOp; } const skillOp = pythonSkillOperation(line); if (skillOp) { return skillOp; } if (index > 0 && PYTHON_DEFINITION_PATTERN.test(line)) { const previous = lines[index - 1] ?? ""; if (PYTHON_DECORATOR_PATTERN.test(previous.trim())) { return `${previous.trim()} ${line.trim()}`; } } if (PYTHON_CONTROL_PATTERN.test(line)) { const childIndex = firstPythonChildLine(lines, index); if (childIndex !== undefined) { return `${line.trim().replace(/:\s*$/, ":")} ${simplifyPythonPreviewLine(lines[childIndex] ?? "", paths)}`; } } return simplifyPythonPreviewLine(line, paths); } function isSkippablePythonChildLine(line: string): boolean { const trimmed = line.trim(); return ( isSkippablePythonLine(trimmed) || PYTHON_DECORATOR_PATTERN.test(trimmed) || PYTHON_LOW_SIGNAL_STATEMENT_PATTERN.test(trimmed) || PYTHON_SLEEP_CALL_PATTERN.test(trimmed) || PYTHON_CLEANUP_CALL_PATTERN.test(trimmed) ); } function firstPythonChildLine(lines: readonly string[], parentIndex: number): number | undefined { const parentIndent = pythonIndent(lines[parentIndex] ?? ""); let fallbackChildIndex: number | undefined; for (let i = parentIndex + 1; i < lines.length; i++) { const line = lines[i] ?? ""; if (isSkippablePythonLine(line) || PYTHON_DECORATOR_PATTERN.test(line.trim())) { continue; } if (pythonIndent(line) <= parentIndent) { break; } if (isSkippablePythonChildLine(line)) { if (fallbackChildIndex === undefined) { fallbackChildIndex = i; } continue; } return i; } return fallbackChildIndex; } function pythonLineScore(lines: readonly string[], index: number, paths: ReadonlyMap): number { const line = lines[index] ?? ""; const trimmed = line.trim(); if (isSkippablePythonLine(line) || PYTHON_DECORATOR_PATTERN.test(trimmed) || /^[)\]},;\s]+(?:#.*)?$/.test(trimmed)) { return -1; } const fileOp = pythonFileOperation(line, paths); if (fileOp) { const isMutation = fileOp.startsWith("write ") || fileOp.startsWith("delete ") || fileOp.startsWith("replace ") || fileOp.startsWith("rename "); return isMutation ? 95 : 92; } if (EDIT_SKILL_PATTERN.test(trimmed)) { return 95; } if (pythonSubprocessCommand(line) || BASH_SKILL_CALL_PATTERN.test(trimmed)) { return 90; } if (DYNAMIC_BASH_SKILL_CALL_PATTERN.test(trimmed)) { return 88; } if (pythonSkillOperation(line)) { return 85; } if (PYTHON_EFFECT_CALL_PATTERN.test(line)) { return 80; } if (PYTHON_MAIN_PATTERN.test(line)) { return 70; } if (PYTHON_CONTROL_PATTERN.test(line)) { const childIndex = firstPythonChildLine(lines, index); return childIndex === undefined ? 20 : Math.max(20, pythonLineScore(lines, childIndex, paths) - 5); } if (PYTHON_DEFINITION_PATTERN.test(line)) { return 50; } if (PYTHON_LOW_SIGNAL_ASSIGNMENT_CALL_PATTERN.test(line)) { return 25; } const printInnerCall = pythonPrintInnerCall(line); if (printInnerCall && !PYTHON_LOW_SIGNAL_CALL_PATTERN.test(printInnerCall)) { return 55; } if (PYTHON_ASSIGNMENT_CALL_PATTERN.test(line)) { return 60; } if (PYTHON_CLEANUP_CALL_PATTERN.test(trimmed)) { return 8; } if (PYTHON_SLEEP_CALL_PATTERN.test(trimmed)) { return 10; } if (PYTHON_LOW_SIGNAL_STATEMENT_PATTERN.test(trimmed)) { return 5; } if (PYTHON_CALL_PATTERN.test(line) && !PYTHON_LOW_SIGNAL_CALL_PATTERN.test(line)) { return 65; } if (PYTHON_CALL_PATTERN.test(line)) { return 15; } return 30; } function pythonPreviewIndex(lines: readonly string[], index: number, paths: ReadonlyMap): number { const line = lines[index] ?? ""; if (!PYTHON_CONTROL_PATTERN.test(line) || pythonFileOperation(line, paths)) { return index; } const childIndex = firstPythonChildLine(lines, index); if (childIndex === undefined) { return index; } const childLine = lines[childIndex] ?? ""; if (PYTHON_LOW_SIGNAL_STATEMENT_PATTERN.test(childLine.trim())) { return index; } return pythonPreviewIndex(lines, childIndex, paths); } const PYTHON_ESCAPES: Record = { "\n": "", // backslash-newline is a line continuation '"': '"', "'": "'", "\\": "\\", n: "\n", r: "\r", t: " ", }; interface PythonStringScan { value: string; end: number; closed: boolean; /** Saw a cooked escape (\x, \u, octal, \a…) whose value is not computed here. */ unsupportedEscape: boolean; } // Walks a python string-literal body from just after the opening delimiter, // following python's escape rules (in raw strings backslash-quote never closes). function scanPythonStringLiteral(code: string, start: number, quote: string, raw: boolean): PythonStringScan { let value = ""; let i = start; let unsupportedEscape = false; while (i < code.length) { const char = code[i] ?? ""; if (char === "\\" && i + 1 < code.length) { const next = code[i + 1] ?? ""; if (!raw && /[xuUN0-7abfv]/.test(next)) { unsupportedEscape = true; } value += raw ? char + next : (PYTHON_ESCAPES[next] ?? char + next); i += 2; continue; } if (code.startsWith(quote, i)) { return { value, end: i + quote.length, closed: true, unsupportedEscape }; } if (quote.length === 1 && char === "\n") { break; // single-quoted literals cannot span lines } value += char; i += 1; } return { value, end: i, closed: false, unsupportedEscape }; } /** Keep source-line positions while masking multiline-string continuations. */ export function pythonStatementLines(code: string): string[] { const lines = code.split("\n"); let line = 0; let i = 0; while (i < code.length) { const char = code[i]!; if (char === "#") { const newline = code.indexOf("\n", i); if (newline < 0) break; i = newline; continue; } if (char === '"' || char === "'") { const quote = code.startsWith(char.repeat(3), i) ? char.repeat(3) : char; const scan = scanPythonStringLiteral(code, i + quote.length, quote, true); const startLine = line; for (let end = i; end < scan.end; end++) { if (code[end] === "\n") lines[++line] = ""; } if (scan.closed && line > startLine) { const column = scan.end - code.lastIndexOf("\n", scan.end - 1) - 1; const newline = code.indexOf("\n", scan.end); lines[line] = " ".repeat(column) + code.slice(scan.end, newline < 0 ? undefined : newline); } i = scan.end; continue; } if (char === "\n") line++; i++; } return lines; } function extractBashSkillCommand(code: string): string | undefined { const match = code.match(BASH_SKILL_CALL_PATTERN); const quote = match?.[1]; if (!match || !quote) return undefined; const start = match[0].length; const prefixChar = match[0][start - quote.length - 1]; const scan = scanPythonStringLiteral(code, start, quote, prefixChar === "r" || prefixChar === "R"); if (!scan.closed || scan.unsupportedEscape) return undefined; const rest = code.slice(scan.end).trimStart(); // Require a plain literal first argument; concatenation or other expressions fall back. if (!rest.startsWith(",") && !rest.startsWith(")")) return undefined; return scan.value; } export function previewPythonCode(code: string): CodePreview { const rawLines = code.split("\n"); const lines = pythonStatementLines(code).map((line) => line.replace(/^(\s*);\s*/, "$1")); const paths = pythonPathVars(lines); let bestIndex: number | undefined; let bestScore = -1; for (let i = 0; i < lines.length; i++) { const score = pythonLineScore(lines, i, paths); if (score > bestScore) { bestIndex = i; bestScore = score; } } if (bestIndex !== undefined && bestScore >= 0) { const previewIndex = pythonPreviewIndex(lines, bestIndex, paths); // Keep the original tail for multiline commands, excluding any preceding string continuation. const bashCommand = extractBashSkillCommand( [lines[previewIndex], ...rawLines.slice(previewIndex + 1)].join("\n"), ); if (bashCommand) { return previewBashCommand(bashCommand); } return { language: "python", text: descriptor(pythonPreviewLine(lines, previewIndex, paths)), }; } return { language: "python", text: "" }; } export function previewIpythonCode(code: string): CodePreview { const trimmedCode = code.trimEnd(); const bashCell = parseIpythonBashCell(trimmedCode); if (bashCell) { return previewBashCommand(bashCell.body); } return previewPythonCode(trimmedCode); }