{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAAC,SAAS,CAErD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAAC,SAAS,CAEjD;AA6BD,eAAO,MAAM,aAAa,QAC2H,CAAC;AAoKtJ;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAuDhD;AAWD,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG3D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAsCjG;AAmWD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAuBtE;AAmFD,eAAO,MAAM,iBAAiB,QAAyC,CAAC;AAExE;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD;AAuED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM,CASzG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC9B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,MAAc,EACxB,GAAG,GAAE,OAAe,GAClB,MAAM,CAmIR;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAQ,GAAG,MAAM,CAEpG;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAC7B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,UAAQ,GACZ;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAwCjC;AAKD;;;;GAIG;AACH,wBAAgB,eAAe,CAC9B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,WAAW,UAAQ,GACjB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAiE5E","sourcesContent":["import { eastAsianWidth } from \"get-east-asian-width\";\n\n// segmenters (shared instance)\nconst graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: \"grapheme\" });\nconst wordSegmenter = new Intl.Segmenter(undefined, { granularity: \"word\" });\n\n/**\n * Get the shared grapheme segmenter instance.\n */\nexport function getGraphemeSegmenter(): Intl.Segmenter {\n\treturn graphemeSegmenter;\n}\n\n/**\n * Get the shared word segmenter instance.\n */\nexport function getWordSegmenter(): Intl.Segmenter {\n\treturn wordSegmenter;\n}\n\n/**\n * Check if a grapheme cluster (after segmentation) could possibly be an RGI emoji.\n * This is a fast heuristic to avoid the expensive rgiEmojiRegex test.\n * The tested Unicode blocks are deliberately broad to account for future\n * Unicode additions.\n */\nfunction couldBeEmoji(segment: string): boolean {\n\tconst cp = segment.codePointAt(0)!;\n\treturn (\n\t\t(cp >= 0x1f000 && cp <= 0x1fbff) || // Emoji and Pictograph\n\t\t(cp >= 0x2300 && cp <= 0x23ff) || // Misc technical\n\t\t(cp >= 0x2600 && cp <= 0x27bf) || // Misc symbols, dingbats\n\t\t(cp >= 0x2b50 && cp <= 0x2b55) || // Specific stars/circles\n\t\tsegment.includes(\"\\uFE0F\") || // Contains VS16 (emoji presentation selector)\n\t\tsegment.length > 2 // Multi-codepoint sequences (ZWJ, skin tones, etc.)\n\t);\n}\n\n// Regexes for character classification (same as string-width library)\nconst zeroWidthRegex = /^(?:\\p{Default_Ignorable_Code_Point}|\\p{Control}|\\p{Mark}|\\p{Surrogate})+$/v;\nconst leadingNonPrintingRegex = /^[\\p{Default_Ignorable_Code_Point}\\p{Control}\\p{Format}\\p{Mark}\\p{Surrogate}]+/v;\nconst rgiEmojiRegex = /^\\p{RGI_Emoji}$/v;\n\n// Cache for non-ASCII strings\nconst WIDTH_CACHE_SIZE = 512;\nconst widthCache = new Map<string, number>();\n\nexport const cjkBreakRegex =\n\t/[\\p{Script_Extensions=Han}\\p{Script_Extensions=Hiragana}\\p{Script_Extensions=Katakana}\\p{Script_Extensions=Hangul}\\p{Script_Extensions=Bopomofo}]/u;\n\nfunction isPrintableAscii(str: string): boolean {\n\tfor (let i = 0; i < str.length; i++) {\n\t\tconst code = str.charCodeAt(i);\n\t\tif (code < 0x20 || code > 0x7e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nfunction truncateFragmentToWidth(text: string, maxWidth: number): { text: string; width: number } {\n\tif (maxWidth <= 0 || text.length === 0) {\n\t\treturn { text: \"\", width: 0 };\n\t}\n\n\tif (isPrintableAscii(text)) {\n\t\tconst clipped = text.slice(0, maxWidth);\n\t\treturn { text: clipped, width: clipped.length };\n\t}\n\n\tconst hasAnsi = text.includes(\"\\x1b\");\n\tconst hasTabs = text.includes(\"\\t\");\n\tif (!hasAnsi && !hasTabs) {\n\t\tlet result = \"\";\n\t\tlet width = 0;\n\t\tfor (const { segment } of graphemeSegmenter.segment(text)) {\n\t\t\tconst w = graphemeWidth(segment);\n\t\t\tif (width + w > maxWidth) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tresult += segment;\n\t\t\twidth += w;\n\t\t}\n\t\treturn { text: result, width };\n\t}\n\n\tlet result = \"\";\n\tlet width = 0;\n\tlet i = 0;\n\tlet pendingAnsi = \"\";\n\n\twhile (i < text.length) {\n\t\tconst ansi = extractAnsiCode(text, i);\n\t\tif (ansi) {\n\t\t\tpendingAnsi += ansi.code;\n\t\t\ti += ansi.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (text[i] === \"\\t\") {\n\t\t\tif (width + 3 > maxWidth) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (pendingAnsi) {\n\t\t\t\tresult += pendingAnsi;\n\t\t\t\tpendingAnsi = \"\";\n\t\t\t}\n\t\t\tresult += \"\\t\";\n\t\t\twidth += 3;\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet end = i;\n\t\twhile (end < text.length && text[end] !== \"\\t\") {\n\t\t\tconst nextAnsi = extractAnsiCode(text, end);\n\t\t\tif (nextAnsi) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tend++;\n\t\t}\n\n\t\tfor (const { segment } of graphemeSegmenter.segment(text.slice(i, end))) {\n\t\t\tconst w = graphemeWidth(segment);\n\t\t\tif (width + w > maxWidth) {\n\t\t\t\treturn { text: result, width };\n\t\t\t}\n\t\t\tif (pendingAnsi) {\n\t\t\t\tresult += pendingAnsi;\n\t\t\t\tpendingAnsi = \"\";\n\t\t\t}\n\t\t\tresult += segment;\n\t\t\twidth += w;\n\t\t}\n\t\ti = end;\n\t}\n\n\treturn { text: result, width };\n}\n\nfunction finalizeTruncatedResult(\n\tprefix: string,\n\tprefixWidth: number,\n\tellipsis: string,\n\tellipsisWidth: number,\n\tmaxWidth: number,\n\tpad: boolean,\n): string {\n\tconst reset = \"\\x1b[0m\";\n\tconst visibleWidth = prefixWidth + ellipsisWidth;\n\tlet result: string;\n\n\tif (ellipsis.length > 0) {\n\t\tresult = `${prefix}${reset}${ellipsis}${reset}`;\n\t} else {\n\t\tresult = `${prefix}${reset}`;\n\t}\n\n\treturn pad ? result + \" \".repeat(Math.max(0, maxWidth - visibleWidth)) : result;\n}\n\n/**\n * Calculate the terminal width of a single grapheme cluster.\n * Based on code from the string-width library, but includes a possible-emoji\n * check to avoid running the RGI_Emoji regex unnecessarily.\n */\nfunction graphemeWidth(segment: string): number {\n\tif (segment === \"\\t\") {\n\t\treturn 3;\n\t}\n\n\t// Zero-width clusters\n\tif (zeroWidthRegex.test(segment)) {\n\t\treturn 0;\n\t}\n\n\t// Emoji check with pre-filter\n\tif (couldBeEmoji(segment) && rgiEmojiRegex.test(segment)) {\n\t\treturn 2;\n\t}\n\n\t// Get base visible codepoint\n\tconst base = segment.replace(leadingNonPrintingRegex, \"\");\n\tconst cp = base.codePointAt(0);\n\tif (cp === undefined) {\n\t\treturn 0;\n\t}\n\n\t// Regional indicator symbols (U+1F1E6..U+1F1FF) are often rendered as\n\t// full-width emoji in terminals, even when isolated during streaming.\n\t// Keep width conservative (2) to avoid terminal auto-wrap drift artifacts.\n\tif (cp >= 0x1f1e6 && cp <= 0x1f1ff) {\n\t\treturn 2;\n\t}\n\n\tlet width = eastAsianWidth(cp);\n\n\t// Trailing halfwidth/fullwidth forms and AM vowels that segment with a base.\n\tif (segment.length > 1) {\n\t\tfor (const char of segment.slice(1)) {\n\t\t\tconst c = char.codePointAt(0)!;\n\t\t\tif (c >= 0xff00 && c <= 0xffef) {\n\t\t\t\twidth += eastAsianWidth(c);\n\t\t\t} else if (c === 0x0e33 || c === 0x0eb3) {\n\t\t\t\twidth += 1;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn width;\n}\n\n/**\n * Calculate the visible width of a string in terminal columns.\n */\nexport function visibleWidth(str: string): number {\n\tif (str.length === 0) {\n\t\treturn 0;\n\t}\n\n\t// Fast path: pure ASCII printable\n\tif (isPrintableAscii(str)) {\n\t\treturn str.length;\n\t}\n\n\t// Check cache\n\tconst cached = widthCache.get(str);\n\tif (cached !== undefined) {\n\t\treturn cached;\n\t}\n\n\t// Normalize: tabs to 3 spaces, strip ANSI escape codes\n\tlet clean = str;\n\tif (str.includes(\"\\t\")) {\n\t\tclean = clean.replace(/\\t/g, \"   \");\n\t}\n\tif (clean.includes(\"\\x1b\")) {\n\t\t// Strip supported ANSI/OSC/APC escape sequences in one pass.\n\t\t// This covers CSI styling/cursor codes, OSC hyperlinks and prompt markers,\n\t\t// and APC sequences like CURSOR_MARKER.\n\t\tlet stripped = \"\";\n\t\tlet i = 0;\n\t\twhile (i < clean.length) {\n\t\t\tconst ansi = extractAnsiCode(clean, i);\n\t\t\tif (ansi) {\n\t\t\t\ti += ansi.length;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tstripped += clean[i];\n\t\t\ti++;\n\t\t}\n\t\tclean = stripped;\n\t}\n\n\t// Calculate width\n\tlet width = 0;\n\tfor (const { segment } of graphemeSegmenter.segment(clean)) {\n\t\twidth += graphemeWidth(segment);\n\t}\n\n\t// Cache result\n\tif (widthCache.size >= WIDTH_CACHE_SIZE) {\n\t\tconst firstKey = widthCache.keys().next().value;\n\t\tif (firstKey !== undefined) {\n\t\t\twidthCache.delete(firstKey);\n\t\t}\n\t}\n\twidthCache.set(str, width);\n\n\treturn width;\n}\n\n/**\n * Normalize text for terminal output without changing logical editor content.\n * Some terminals render precomposed Thai/Lao AM vowels inconsistently during\n * differential repaint. Their compatibility decompositions have the same cell\n * width but avoid stale-cell artifacts in terminal renderers.\n */\nconst THAI_LAO_AM_REGEX = /[\\u0e33\\u0eb3]/;\nconst THAI_LAO_AM_GLOBAL_REGEX = /[\\u0e33\\u0eb3]/g;\n\nexport function normalizeTerminalOutput(str: string): string {\n\tif (!THAI_LAO_AM_REGEX.test(str)) return str;\n\treturn str.replace(THAI_LAO_AM_GLOBAL_REGEX, (char) => (char === \"\\u0e33\" ? \"\\u0e4d\\u0e32\" : \"\\u0ecd\\u0eb2\"));\n}\n\n/**\n * Extract ANSI escape sequences from a string at the given position.\n */\nexport function extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {\n\tif (pos >= str.length || str[pos] !== \"\\x1b\") return null;\n\n\tconst next = str[pos + 1];\n\n\t// CSI sequence: ESC [ ... m/G/K/H/J\n\tif (next === \"[\") {\n\t\tlet j = pos + 2;\n\t\twhile (j < str.length && !/[mGKHJ]/.test(str[j]!)) j++;\n\t\tif (j < str.length) return { code: str.substring(pos, j + 1), length: j + 1 - pos };\n\t\treturn null;\n\t}\n\n\t// OSC sequence: ESC ] ... BEL or ESC ] ... ST (ESC \\)\n\t// Used for hyperlinks (OSC 8), window titles, etc.\n\tif (next === \"]\") {\n\t\tlet j = pos + 2;\n\t\twhile (j < str.length) {\n\t\t\tif (str[j] === \"\\x07\") return { code: str.substring(pos, j + 1), length: j + 1 - pos };\n\t\t\tif (str[j] === \"\\x1b\" && str[j + 1] === \"\\\\\") return { code: str.substring(pos, j + 2), length: j + 2 - pos };\n\t\t\tj++;\n\t\t}\n\t\treturn null;\n\t}\n\n\t// APC sequence: ESC _ ... BEL or ESC _ ... ST (ESC \\)\n\t// Used for cursor marker and application-specific commands\n\tif (next === \"_\") {\n\t\tlet j = pos + 2;\n\t\twhile (j < str.length) {\n\t\t\tif (str[j] === \"\\x07\") return { code: str.substring(pos, j + 1), length: j + 1 - pos };\n\t\t\tif (str[j] === \"\\x1b\" && str[j + 1] === \"\\\\\") return { code: str.substring(pos, j + 2), length: j + 2 - pos };\n\t\t\tj++;\n\t\t}\n\t\treturn null;\n\t}\n\n\treturn null;\n}\n\ntype Osc8Terminator = \"\\x07\" | \"\\x1b\\\\\";\n\ninterface ActiveHyperlink {\n\tparams: string;\n\turl: string;\n\tterminator: Osc8Terminator;\n}\n\nfunction parseOsc8Hyperlink(ansiCode: string): ActiveHyperlink | null | undefined {\n\tif (!ansiCode.startsWith(\"\\x1b]8;\")) {\n\t\treturn undefined;\n\t}\n\n\tconst terminator: Osc8Terminator = ansiCode.endsWith(\"\\x07\") ? \"\\x07\" : \"\\x1b\\\\\";\n\tconst body = ansiCode.slice(4, terminator === \"\\x07\" ? -1 : -2);\n\tconst separatorIndex = body.indexOf(\";\");\n\tif (separatorIndex === -1) {\n\t\treturn undefined;\n\t}\n\n\tconst params = body.slice(0, separatorIndex);\n\tconst url = body.slice(separatorIndex + 1);\n\tif (!url) {\n\t\treturn null;\n\t}\n\treturn { params, url, terminator };\n}\n\nfunction formatOsc8Hyperlink(hyperlink: ActiveHyperlink): string {\n\treturn `\\x1b]8;${hyperlink.params};${hyperlink.url}${hyperlink.terminator}`;\n}\n\nfunction formatOsc8Close(terminator: Osc8Terminator): string {\n\treturn `\\x1b]8;;${terminator}`;\n}\n\n/**\n * Track active ANSI SGR codes to preserve styling across line breaks.\n */\nclass AnsiCodeTracker {\n\t// Track individual attributes separately so we can reset them specifically\n\tprivate bold = false;\n\tprivate dim = false;\n\tprivate italic = false;\n\tprivate underline = false;\n\tprivate blink = false;\n\tprivate inverse = false;\n\tprivate hidden = false;\n\tprivate strikethrough = false;\n\tprivate fgColor: string | null = null; // Stores the full code like \"31\" or \"38;5;240\"\n\tprivate bgColor: string | null = null; // Stores the full code like \"41\" or \"48;5;240\"\n\tprivate activeHyperlink: ActiveHyperlink | null = null;\n\n\tprocess(ansiCode: string): void {\n\t\t// OSC 8 hyperlink: \\x1b]8;;<url>\\x1b\\\\ (open) or \\x1b]8;;\\x1b\\\\ (close).\n\t\t// Preserve the original terminator because some terminals only make BEL-terminated\n\t\t// links clickable. OAuth login URLs use BEL, so reopening wrapped lines with ST\n\t\t// made only the first physical line clickable in those terminals.\n\t\tconst hyperlink = parseOsc8Hyperlink(ansiCode);\n\t\tif (hyperlink !== undefined) {\n\t\t\tthis.activeHyperlink = hyperlink;\n\t\t\treturn;\n\t\t}\n\n\t\tif (!ansiCode.endsWith(\"m\")) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Extract the parameters between \\x1b[ and m\n\t\tconst match = ansiCode.match(/\\x1b\\[([\\d;]*)m/);\n\t\tif (!match) return;\n\n\t\tconst params = match[1];\n\t\tif (params === \"\" || params === \"0\") {\n\t\t\t// Full reset\n\t\t\tthis.reset();\n\t\t\treturn;\n\t\t}\n\n\t\t// Parse parameters (can be semicolon-separated)\n\t\tconst parts = params.split(\";\");\n\t\tlet i = 0;\n\t\twhile (i < parts.length) {\n\t\t\tconst code = Number.parseInt(parts[i], 10);\n\n\t\t\t// Handle 256-color and RGB codes which consume multiple parameters\n\t\t\tif (code === 38 || code === 48) {\n\t\t\t\t// 38;5;N (256 color fg) or 38;2;R;G;B (RGB fg)\n\t\t\t\t// 48;5;N (256 color bg) or 48;2;R;G;B (RGB bg)\n\t\t\t\tif (parts[i + 1] === \"5\" && parts[i + 2] !== undefined) {\n\t\t\t\t\t// 256 color: 38;5;N or 48;5;N\n\t\t\t\t\tconst colorCode = `${parts[i]};${parts[i + 1]};${parts[i + 2]}`;\n\t\t\t\t\tif (code === 38) {\n\t\t\t\t\t\tthis.fgColor = colorCode;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.bgColor = colorCode;\n\t\t\t\t\t}\n\t\t\t\t\ti += 3;\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (parts[i + 1] === \"2\" && parts[i + 4] !== undefined) {\n\t\t\t\t\t// RGB color: 38;2;R;G;B or 48;2;R;G;B\n\t\t\t\t\tconst colorCode = `${parts[i]};${parts[i + 1]};${parts[i + 2]};${parts[i + 3]};${parts[i + 4]}`;\n\t\t\t\t\tif (code === 38) {\n\t\t\t\t\t\tthis.fgColor = colorCode;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.bgColor = colorCode;\n\t\t\t\t\t}\n\t\t\t\t\ti += 5;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Standard SGR codes\n\t\t\tswitch (code) {\n\t\t\t\tcase 0:\n\t\t\t\t\tthis.reset();\n\t\t\t\t\tbreak;\n\t\t\t\tcase 1:\n\t\t\t\t\tthis.bold = true;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 2:\n\t\t\t\t\tthis.dim = true;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 3:\n\t\t\t\t\tthis.italic = true;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 4:\n\t\t\t\t\tthis.underline = true;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 5:\n\t\t\t\t\tthis.blink = true;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 7:\n\t\t\t\t\tthis.inverse = true;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 8:\n\t\t\t\t\tthis.hidden = true;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 9:\n\t\t\t\t\tthis.strikethrough = true;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 21:\n\t\t\t\t\tthis.bold = false;\n\t\t\t\t\tbreak; // Some terminals\n\t\t\t\tcase 22:\n\t\t\t\t\tthis.bold = false;\n\t\t\t\t\tthis.dim = false;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 23:\n\t\t\t\t\tthis.italic = false;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 24:\n\t\t\t\t\tthis.underline = false;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 25:\n\t\t\t\t\tthis.blink = false;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 27:\n\t\t\t\t\tthis.inverse = false;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 28:\n\t\t\t\t\tthis.hidden = false;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 29:\n\t\t\t\t\tthis.strikethrough = false;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 39:\n\t\t\t\t\tthis.fgColor = null;\n\t\t\t\t\tbreak; // Default fg\n\t\t\t\tcase 49:\n\t\t\t\t\tthis.bgColor = null;\n\t\t\t\t\tbreak; // Default bg\n\t\t\t\tdefault:\n\t\t\t\t\t// Standard foreground colors 30-37, 90-97\n\t\t\t\t\tif ((code >= 30 && code <= 37) || (code >= 90 && code <= 97)) {\n\t\t\t\t\t\tthis.fgColor = String(code);\n\t\t\t\t\t}\n\t\t\t\t\t// Standard background colors 40-47, 100-107\n\t\t\t\t\telse if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) {\n\t\t\t\t\t\tthis.bgColor = String(code);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\ti++;\n\t\t}\n\t}\n\n\tprivate reset(): void {\n\t\tthis.bold = false;\n\t\tthis.dim = false;\n\t\tthis.italic = false;\n\t\tthis.underline = false;\n\t\tthis.blink = false;\n\t\tthis.inverse = false;\n\t\tthis.hidden = false;\n\t\tthis.strikethrough = false;\n\t\tthis.fgColor = null;\n\t\tthis.bgColor = null;\n\t\t// SGR reset does not affect OSC 8 hyperlink state\n\t}\n\n\t/** Clear all state for reuse. */\n\tclear(): void {\n\t\tthis.reset();\n\t\tthis.activeHyperlink = null;\n\t}\n\n\tgetActiveCodes(): string {\n\t\tconst codes: string[] = [];\n\t\tif (this.bold) codes.push(\"1\");\n\t\tif (this.dim) codes.push(\"2\");\n\t\tif (this.italic) codes.push(\"3\");\n\t\tif (this.underline) codes.push(\"4\");\n\t\tif (this.blink) codes.push(\"5\");\n\t\tif (this.inverse) codes.push(\"7\");\n\t\tif (this.hidden) codes.push(\"8\");\n\t\tif (this.strikethrough) codes.push(\"9\");\n\t\tif (this.fgColor) codes.push(this.fgColor);\n\t\tif (this.bgColor) codes.push(this.bgColor);\n\n\t\tlet result = codes.length > 0 ? `\\x1b[${codes.join(\";\")}m` : \"\";\n\t\tif (this.activeHyperlink) {\n\t\t\tresult += formatOsc8Hyperlink(this.activeHyperlink);\n\t\t}\n\t\treturn result;\n\t}\n\n\thasActiveCodes(): boolean {\n\t\treturn (\n\t\t\tthis.bold ||\n\t\t\tthis.dim ||\n\t\t\tthis.italic ||\n\t\t\tthis.underline ||\n\t\t\tthis.blink ||\n\t\t\tthis.inverse ||\n\t\t\tthis.hidden ||\n\t\t\tthis.strikethrough ||\n\t\t\tthis.fgColor !== null ||\n\t\t\tthis.bgColor !== null ||\n\t\t\tthis.activeHyperlink !== null\n\t\t);\n\t}\n\n\t/**\n\t * Get reset codes for attributes that need to be turned off at line end.\n\t * Underline must be closed to prevent bleeding into padding.\n\t * Active OSC 8 hyperlinks must be closed and re-opened on the next line.\n\t * Returns empty string if no attributes need closing.\n\t */\n\tgetLineEndReset(): string {\n\t\tlet result = \"\";\n\t\tif (this.underline) {\n\t\t\tresult += \"\\x1b[24m\"; // Underline off only\n\t\t}\n\t\tif (this.activeHyperlink) {\n\t\t\tresult += formatOsc8Close(this.activeHyperlink.terminator); // Re-opened at line start via getActiveCodes()\n\t\t}\n\t\treturn result;\n\t}\n}\n\nfunction updateTrackerFromText(text: string, tracker: AnsiCodeTracker): void {\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n}\n\n/**\n * Split text into words while keeping ANSI codes attached.\n */\nfunction splitIntoTokensWithAnsi(text: string): string[] {\n\tconst tokens: string[] = [];\n\tlet current = \"\";\n\tlet pendingAnsi = \"\"; // ANSI codes waiting to be attached to next visible content\n\tlet currentKind: \"space\" | \"word\" | null = null;\n\tlet i = 0;\n\n\tconst flushCurrent = (): void => {\n\t\tif (!current) {\n\t\t\treturn;\n\t\t}\n\t\ttokens.push(current);\n\t\tcurrent = \"\";\n\t\tcurrentKind = null;\n\t};\n\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\t// Hold ANSI codes separately - they'll be attached to the next visible char\n\t\t\tpendingAnsi += ansiResult.code;\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet end = i;\n\t\twhile (end < text.length && !extractAnsiCode(text, end)) {\n\t\t\tend++;\n\t\t}\n\n\t\tfor (const { segment } of graphemeSegmenter.segment(text.slice(i, end))) {\n\t\t\tconst segmentIsSpace = segment === \" \";\n\t\t\tif (!segmentIsSpace && cjkBreakRegex.test(segment)) {\n\t\t\t\tflushCurrent();\n\t\t\t\tconst token = pendingAnsi + segment;\n\t\t\t\tpendingAnsi = \"\";\n\t\t\t\ttokens.push(token);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst segmentKind = segmentIsSpace ? \"space\" : \"word\";\n\t\t\tif (current && currentKind !== segmentKind) {\n\t\t\t\tflushCurrent();\n\t\t\t}\n\n\t\t\t// Attach any pending ANSI codes to this visible character\n\t\t\tif (pendingAnsi) {\n\t\t\t\tcurrent += pendingAnsi;\n\t\t\t\tpendingAnsi = \"\";\n\t\t\t}\n\n\t\t\tcurrentKind = segmentKind;\n\t\t\tcurrent += segment;\n\t\t}\n\n\t\ti = end;\n\t}\n\n\t// Handle any remaining pending ANSI codes (attach to last token)\n\tif (pendingAnsi) {\n\t\tif (current) {\n\t\t\tcurrent += pendingAnsi;\n\t\t} else if (tokens.length > 0) {\n\t\t\ttokens[tokens.length - 1] += pendingAnsi;\n\t\t} else {\n\t\t\tcurrent = pendingAnsi;\n\t\t}\n\t}\n\n\tif (current) {\n\t\ttokens.push(current);\n\t}\n\n\treturn tokens;\n}\n\n/**\n * Wrap text with ANSI codes preserved.\n *\n * ONLY does word wrapping - NO padding, NO background colors.\n * Returns lines where each line is <= width visible chars.\n * Active ANSI codes are preserved across line breaks.\n *\n * @param text - Text to wrap (may contain ANSI codes and newlines)\n * @param width - Maximum visible width per line\n * @returns Array of wrapped lines (NOT padded to width)\n */\nexport function wrapTextWithAnsi(text: string, width: number): string[] {\n\tif (!text) {\n\t\treturn [\"\"];\n\t}\n\n\t// Handle newlines by processing each line separately\n\t// Track ANSI state across lines so styles carry over after literal newlines\n\tconst inputLines = text.split(\"\\n\");\n\tconst result: string[] = [];\n\tconst tracker = new AnsiCodeTracker();\n\n\tfor (const inputLine of inputLines) {\n\t\t// Prepend active ANSI codes from previous lines (except for first line)\n\t\tconst prefix = result.length > 0 ? tracker.getActiveCodes() : \"\";\n\t\tconst wrappedLines = wrapSingleLine(prefix + inputLine, width);\n\t\tfor (const wrappedLine of wrappedLines) {\n\t\t\tresult.push(wrappedLine);\n\t\t}\n\t\t// Update tracker with codes from this line for next iteration\n\t\tupdateTrackerFromText(inputLine, tracker);\n\t}\n\n\treturn result.length > 0 ? result : [\"\"];\n}\n\nfunction wrapSingleLine(line: string, width: number): string[] {\n\tif (!line) {\n\t\treturn [\"\"];\n\t}\n\n\tconst visibleLength = visibleWidth(line);\n\tif (visibleLength <= width) {\n\t\treturn [line];\n\t}\n\n\tconst wrapped: string[] = [];\n\tconst tracker = new AnsiCodeTracker();\n\tconst tokens = splitIntoTokensWithAnsi(line);\n\n\tlet currentLine = \"\";\n\tlet currentVisibleLength = 0;\n\n\tfor (const token of tokens) {\n\t\tconst tokenVisibleLength = visibleWidth(token);\n\t\tconst isWhitespace = token.trim() === \"\";\n\n\t\t// Token itself is too long - break it character by character\n\t\tif (tokenVisibleLength > width && !isWhitespace) {\n\t\t\tif (currentLine) {\n\t\t\t\t// Add specific reset for underline only (preserves background)\n\t\t\t\tconst lineEndReset = tracker.getLineEndReset();\n\t\t\t\tif (lineEndReset) {\n\t\t\t\t\tcurrentLine += lineEndReset;\n\t\t\t\t}\n\t\t\t\twrapped.push(currentLine);\n\t\t\t\tcurrentLine = \"\";\n\t\t\t\tcurrentVisibleLength = 0;\n\t\t\t}\n\n\t\t\t// Break long token - breakLongWord handles its own resets\n\t\t\tconst broken = breakLongWord(token, width, tracker);\n\t\t\tfor (let i = 0; i < broken.length - 1; i++) {\n\t\t\t\twrapped.push(broken[i]!);\n\t\t\t}\n\t\t\tcurrentLine = broken[broken.length - 1];\n\t\t\tcurrentVisibleLength = visibleWidth(currentLine);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check if adding this token would exceed width\n\t\tconst totalNeeded = currentVisibleLength + tokenVisibleLength;\n\n\t\tif (totalNeeded > width && currentVisibleLength > 0) {\n\t\t\t// Trim trailing whitespace, then add underline reset (not full reset, to preserve background)\n\t\t\tlet lineToWrap = currentLine.trimEnd();\n\t\t\tconst lineEndReset = tracker.getLineEndReset();\n\t\t\tif (lineEndReset) {\n\t\t\t\tlineToWrap += lineEndReset;\n\t\t\t}\n\t\t\twrapped.push(lineToWrap);\n\t\t\tif (isWhitespace) {\n\t\t\t\t// Don't start new line with whitespace\n\t\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\t\tcurrentVisibleLength = 0;\n\t\t\t} else {\n\t\t\t\tcurrentLine = tracker.getActiveCodes() + token;\n\t\t\t\tcurrentVisibleLength = tokenVisibleLength;\n\t\t\t}\n\t\t} else {\n\t\t\t// Add to current line\n\t\t\tcurrentLine += token;\n\t\t\tcurrentVisibleLength += tokenVisibleLength;\n\t\t}\n\n\t\tupdateTrackerFromText(token, tracker);\n\t}\n\n\tif (currentLine) {\n\t\t// No reset at end of final line - let caller handle it\n\t\twrapped.push(currentLine);\n\t}\n\n\t// Trailing whitespace can cause lines to exceed the requested width\n\treturn wrapped.length > 0 ? wrapped.map((line) => line.trimEnd()) : [\"\"];\n}\n\nexport const PUNCTUATION_REGEX = /[(){}[\\]<>.,;:'\"!?+\\-=*/\\\\|&%^$#@~`]/;\n\n/**\n * Check if a character is whitespace.\n */\nexport function isWhitespaceChar(char: string): boolean {\n\treturn /\\s/.test(char);\n}\n\n/**\n * Check if a character is punctuation.\n */\nexport function isPunctuationChar(char: string): boolean {\n\treturn PUNCTUATION_REGEX.test(char);\n}\n\nfunction breakLongWord(word: string, width: number, tracker: AnsiCodeTracker): string[] {\n\tconst lines: string[] = [];\n\tlet currentLine = tracker.getActiveCodes();\n\tlet currentWidth = 0;\n\n\t// First, separate ANSI codes from visible content\n\t// We need to handle ANSI codes specially since they're not graphemes\n\tlet i = 0;\n\tconst segments: Array<{ type: \"ansi\" | \"grapheme\"; value: string }> = [];\n\n\twhile (i < word.length) {\n\t\tconst ansiResult = extractAnsiCode(word, i);\n\t\tif (ansiResult) {\n\t\t\tsegments.push({ type: \"ansi\", value: ansiResult.code });\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\t// Find the next ANSI code or end of string\n\t\t\tlet end = i;\n\t\t\twhile (end < word.length) {\n\t\t\t\tconst nextAnsi = extractAnsiCode(word, end);\n\t\t\t\tif (nextAnsi) break;\n\t\t\t\tend++;\n\t\t\t}\n\t\t\t// Segment this non-ANSI portion into graphemes\n\t\t\tconst textPortion = word.slice(i, end);\n\t\t\tfor (const seg of graphemeSegmenter.segment(textPortion)) {\n\t\t\t\tsegments.push({ type: \"grapheme\", value: seg.segment });\n\t\t\t}\n\t\t\ti = end;\n\t\t}\n\t}\n\n\t// Now process segments\n\tfor (const seg of segments) {\n\t\tif (seg.type === \"ansi\") {\n\t\t\tcurrentLine += seg.value;\n\t\t\ttracker.process(seg.value);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst grapheme = seg.value;\n\t\t// Skip empty graphemes to avoid issues with string-width calculation\n\t\tif (!grapheme) continue;\n\n\t\tconst graphemeWidth = visibleWidth(grapheme);\n\n\t\tif (currentWidth + graphemeWidth > width) {\n\t\t\t// Add specific reset for underline only (preserves background)\n\t\t\tconst lineEndReset = tracker.getLineEndReset();\n\t\t\tif (lineEndReset) {\n\t\t\t\tcurrentLine += lineEndReset;\n\t\t\t}\n\t\t\tlines.push(currentLine);\n\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\tcurrentWidth = 0;\n\t\t}\n\n\t\tcurrentLine += grapheme;\n\t\tcurrentWidth += graphemeWidth;\n\t}\n\n\tif (currentLine) {\n\t\t// No reset at end of final segment - caller handles continuation\n\t\tlines.push(currentLine);\n\t}\n\n\treturn lines.length > 0 ? lines : [\"\"];\n}\n\n/**\n * Apply background color to a line, padding to full width.\n *\n * @param line - Line of text (may contain ANSI codes)\n * @param width - Total width to pad to\n * @param bgFn - Background color function\n * @returns Line with background applied and padded to width\n */\nexport function applyBackgroundToLine(line: string, width: number, bgFn: (text: string) => string): string {\n\t// Calculate padding needed\n\tconst visibleLen = visibleWidth(line);\n\tconst paddingNeeded = Math.max(0, width - visibleLen);\n\tconst padding = \" \".repeat(paddingNeeded);\n\n\t// Apply background to content + padding\n\tconst withPadding = line + padding;\n\treturn bgFn(withPadding);\n}\n\n/**\n * Truncate text to fit within a maximum visible width, adding ellipsis if needed.\n * Optionally pad with spaces to reach exactly maxWidth.\n * Properly handles ANSI escape codes (they don't count toward width).\n *\n * @param text - Text to truncate (may contain ANSI codes)\n * @param maxWidth - Maximum visible width\n * @param ellipsis - Ellipsis string to append when truncating (default: \"...\")\n * @param pad - If true, pad result with spaces to exactly maxWidth (default: false)\n * @returns Truncated text, optionally padded to exactly maxWidth\n */\nexport function truncateToWidth(\n\ttext: string,\n\tmaxWidth: number,\n\tellipsis: string = \"...\",\n\tpad: boolean = false,\n): string {\n\tif (maxWidth <= 0) {\n\t\treturn \"\";\n\t}\n\n\tif (text.length === 0) {\n\t\treturn pad ? \" \".repeat(maxWidth) : \"\";\n\t}\n\n\tconst ellipsisWidth = visibleWidth(ellipsis);\n\tif (ellipsisWidth >= maxWidth) {\n\t\tconst textWidth = visibleWidth(text);\n\t\tif (textWidth <= maxWidth) {\n\t\t\treturn pad ? text + \" \".repeat(maxWidth - textWidth) : text;\n\t\t}\n\n\t\tconst clippedEllipsis = truncateFragmentToWidth(ellipsis, maxWidth);\n\t\tif (clippedEllipsis.width === 0) {\n\t\t\treturn pad ? \" \".repeat(maxWidth) : \"\";\n\t\t}\n\t\treturn finalizeTruncatedResult(\"\", 0, clippedEllipsis.text, clippedEllipsis.width, maxWidth, pad);\n\t}\n\n\tif (isPrintableAscii(text)) {\n\t\tif (text.length <= maxWidth) {\n\t\t\treturn pad ? text + \" \".repeat(maxWidth - text.length) : text;\n\t\t}\n\t\tconst targetWidth = maxWidth - ellipsisWidth;\n\t\treturn finalizeTruncatedResult(text.slice(0, targetWidth), targetWidth, ellipsis, ellipsisWidth, maxWidth, pad);\n\t}\n\n\tconst targetWidth = maxWidth - ellipsisWidth;\n\tlet result = \"\";\n\tlet pendingAnsi = \"\";\n\tlet visibleSoFar = 0;\n\tlet keptWidth = 0;\n\tlet keepContiguousPrefix = true;\n\tlet overflowed = false;\n\tlet exhaustedInput = false;\n\tconst hasAnsi = text.includes(\"\\x1b\");\n\tconst hasTabs = text.includes(\"\\t\");\n\n\tif (!hasAnsi && !hasTabs) {\n\t\tfor (const { segment } of graphemeSegmenter.segment(text)) {\n\t\t\tconst width = graphemeWidth(segment);\n\t\t\tif (keepContiguousPrefix && keptWidth + width <= targetWidth) {\n\t\t\t\tresult += segment;\n\t\t\t\tkeptWidth += width;\n\t\t\t} else {\n\t\t\t\tkeepContiguousPrefix = false;\n\t\t\t}\n\t\t\tvisibleSoFar += width;\n\t\t\tif (visibleSoFar > maxWidth) {\n\t\t\t\toverflowed = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\texhaustedInput = !overflowed;\n\t} else {\n\t\tlet i = 0;\n\t\twhile (i < text.length) {\n\t\t\tconst ansi = extractAnsiCode(text, i);\n\t\t\tif (ansi) {\n\t\t\t\tpendingAnsi += ansi.code;\n\t\t\t\ti += ansi.length;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (text[i] === \"\\t\") {\n\t\t\t\tif (keepContiguousPrefix && keptWidth + 3 <= targetWidth) {\n\t\t\t\t\tif (pendingAnsi) {\n\t\t\t\t\t\tresult += pendingAnsi;\n\t\t\t\t\t\tpendingAnsi = \"\";\n\t\t\t\t\t}\n\t\t\t\t\tresult += \"\\t\";\n\t\t\t\t\tkeptWidth += 3;\n\t\t\t\t} else {\n\t\t\t\t\tkeepContiguousPrefix = false;\n\t\t\t\t\tpendingAnsi = \"\";\n\t\t\t\t}\n\t\t\t\tvisibleSoFar += 3;\n\t\t\t\tif (visibleSoFar > maxWidth) {\n\t\t\t\t\toverflowed = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\ti++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet end = i;\n\t\t\twhile (end < text.length && text[end] !== \"\\t\") {\n\t\t\t\tconst nextAnsi = extractAnsiCode(text, end);\n\t\t\t\tif (nextAnsi) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tend++;\n\t\t\t}\n\n\t\t\tfor (const { segment } of graphemeSegmenter.segment(text.slice(i, end))) {\n\t\t\t\tconst width = graphemeWidth(segment);\n\t\t\t\tif (keepContiguousPrefix && keptWidth + width <= targetWidth) {\n\t\t\t\t\tif (pendingAnsi) {\n\t\t\t\t\t\tresult += pendingAnsi;\n\t\t\t\t\t\tpendingAnsi = \"\";\n\t\t\t\t\t}\n\t\t\t\t\tresult += segment;\n\t\t\t\t\tkeptWidth += width;\n\t\t\t\t} else {\n\t\t\t\t\tkeepContiguousPrefix = false;\n\t\t\t\t\tpendingAnsi = \"\";\n\t\t\t\t}\n\n\t\t\t\tvisibleSoFar += width;\n\t\t\t\tif (visibleSoFar > maxWidth) {\n\t\t\t\t\toverflowed = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (overflowed) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ti = end;\n\t\t}\n\t\texhaustedInput = i >= text.length;\n\t}\n\n\tif (!overflowed && exhaustedInput) {\n\t\treturn pad ? text + \" \".repeat(Math.max(0, maxWidth - visibleSoFar)) : text;\n\t}\n\n\treturn finalizeTruncatedResult(result, keptWidth, ellipsis, ellipsisWidth, maxWidth, pad);\n}\n\n/**\n * Extract a range of visible columns from a line. Handles ANSI codes and wide chars.\n * @param strict - If true, exclude wide chars at boundary that would extend past the range\n */\nexport function sliceByColumn(line: string, startCol: number, length: number, strict = false): string {\n\treturn sliceWithWidth(line, startCol, length, strict).text;\n}\n\n/** Like sliceByColumn but also returns the actual visible width of the result. */\nexport function sliceWithWidth(\n\tline: string,\n\tstartCol: number,\n\tlength: number,\n\tstrict = false,\n): { text: string; width: number } {\n\tif (length <= 0) return { text: \"\", width: 0 };\n\tconst endCol = startCol + length;\n\tlet result = \"\",\n\t\tresultWidth = 0,\n\t\tcurrentCol = 0,\n\t\ti = 0,\n\t\tpendingAnsi = \"\";\n\n\twhile (i < line.length) {\n\t\tconst ansi = extractAnsiCode(line, i);\n\t\tif (ansi) {\n\t\t\tif (currentCol >= startCol && currentCol < endCol) result += ansi.code;\n\t\t\telse if (currentCol < startCol) pendingAnsi += ansi.code;\n\t\t\ti += ansi.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet textEnd = i;\n\t\twhile (textEnd < line.length && !extractAnsiCode(line, textEnd)) textEnd++;\n\n\t\tfor (const { segment } of graphemeSegmenter.segment(line.slice(i, textEnd))) {\n\t\t\tconst w = graphemeWidth(segment);\n\t\t\tconst inRange = currentCol >= startCol && currentCol < endCol;\n\t\t\tconst fits = !strict || currentCol + w <= endCol;\n\t\t\tif (inRange && fits) {\n\t\t\t\tif (pendingAnsi) {\n\t\t\t\t\tresult += pendingAnsi;\n\t\t\t\t\tpendingAnsi = \"\";\n\t\t\t\t}\n\t\t\t\tresult += segment;\n\t\t\t\tresultWidth += w;\n\t\t\t}\n\t\t\tcurrentCol += w;\n\t\t\tif (currentCol >= endCol) break;\n\t\t}\n\t\ti = textEnd;\n\t\tif (currentCol >= endCol) break;\n\t}\n\treturn { text: result, width: resultWidth };\n}\n\n// Pooled tracker instance for extractSegments (avoids allocation per call)\nconst pooledStyleTracker = new AnsiCodeTracker();\n\n/**\n * Extract \"before\" and \"after\" segments from a line in a single pass.\n * Used for overlay compositing where we need content before and after the overlay region.\n * Preserves styling from before the overlay that should affect content after it.\n */\nexport function extractSegments(\n\tline: string,\n\tbeforeEnd: number,\n\tafterStart: number,\n\tafterLen: number,\n\tstrictAfter = false,\n): { before: string; beforeWidth: number; after: string; afterWidth: number } {\n\tlet before = \"\",\n\t\tbeforeWidth = 0,\n\t\tafter = \"\",\n\t\tafterWidth = 0;\n\tlet currentCol = 0,\n\t\ti = 0;\n\tlet pendingAnsiBefore = \"\";\n\tlet afterStarted = false;\n\tconst afterEnd = afterStart + afterLen;\n\n\t// Track styling state so \"after\" inherits styling from before the overlay\n\tpooledStyleTracker.clear();\n\n\twhile (i < line.length) {\n\t\tconst ansi = extractAnsiCode(line, i);\n\t\tif (ansi) {\n\t\t\t// Track all SGR codes to know styling state at afterStart\n\t\t\tpooledStyleTracker.process(ansi.code);\n\t\t\t// Include ANSI codes in their respective segments\n\t\t\tif (currentCol < beforeEnd) {\n\t\t\t\tpendingAnsiBefore += ansi.code;\n\t\t\t} else if (currentCol >= afterStart && currentCol < afterEnd && afterStarted) {\n\t\t\t\t// Only include after we've started \"after\" (styling already prepended)\n\t\t\t\tafter += ansi.code;\n\t\t\t}\n\t\t\ti += ansi.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet textEnd = i;\n\t\twhile (textEnd < line.length && !extractAnsiCode(line, textEnd)) textEnd++;\n\n\t\tfor (const { segment } of graphemeSegmenter.segment(line.slice(i, textEnd))) {\n\t\t\tconst w = graphemeWidth(segment);\n\n\t\t\tif (currentCol < beforeEnd && currentCol + w <= beforeEnd) {\n\t\t\t\tif (pendingAnsiBefore) {\n\t\t\t\t\tbefore += pendingAnsiBefore;\n\t\t\t\t\tpendingAnsiBefore = \"\";\n\t\t\t\t}\n\t\t\t\tbefore += segment;\n\t\t\t\tbeforeWidth += w;\n\t\t\t} else if (currentCol >= afterStart && currentCol < afterEnd) {\n\t\t\t\tconst fits = !strictAfter || currentCol + w <= afterEnd;\n\t\t\t\tif (fits) {\n\t\t\t\t\t// On first \"after\" grapheme, prepend inherited styling from before overlay\n\t\t\t\t\tif (!afterStarted) {\n\t\t\t\t\t\tafter += pooledStyleTracker.getActiveCodes();\n\t\t\t\t\t\tafterStarted = true;\n\t\t\t\t\t}\n\t\t\t\t\tafter += segment;\n\t\t\t\t\tafterWidth += w;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcurrentCol += w;\n\t\t\t// Early exit: done with \"before\" only, or done with both segments\n\t\t\tif (afterLen <= 0 ? currentCol >= beforeEnd : currentCol >= afterEnd) break;\n\t\t}\n\t\ti = textEnd;\n\t\tif (afterLen <= 0 ? currentCol >= beforeEnd : currentCol >= afterEnd) break;\n\t}\n\n\treturn { before, beforeWidth, after, afterWidth };\n}\n"]}