{"version":3,"file":"header.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/header.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,wBAAwB,CAAC;AAEvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAuKxF,qBAAa,MAAO,YAAW,SAAS;IAEtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;IAC9B,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC;IAFrC,YACkB,YAAY,CAAC,0BAAc,EAC3B,kBAAkB,CAAC,wCAA4B,EAC7D;IAEJ,OAAO,CAAC,OAAO;IAgBf,OAAO,CAAC,aAAa;IAarB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAoD9B;IAED,UAAU,IAAI,IAAI,CAAG;CACrB;AAED,eAAe,MAAM,CAAC","sourcesContent":["import os from \"node:os\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@apholdings/jensen-tui\";\nimport { APP_NAME, VERSION } from \"../../../config.js\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\n\ntype RGB = { r: number; g: number; b: number };\nconst ANSI_ESCAPE_AT_START = /^\\x1b\\[[0-9;]*m/;\n\nconst TITLE = APP_NAME || \"Jensen Code\";\n\nconst VERSION_LABEL = VERSION ? `v${VERSION}` : \"v0.0.0\";\n\nconst LOGO = [\" ████████████ \", \"██▓░░░░░░░░▓██\", \"█░░░░█░░█░░░░█\", \"█░░░░░░░░░░░░█\", \" ████████████ \"];\n\nconst GRADIENT_STOPS: RGB[] = [\n\t{ r: 0x73, g: 0xda, b: 0xca },\n\t{ r: 0x7a, g: 0xa2, b: 0xf7 },\n\t{ r: 0xbb, g: 0x9a, b: 0xf7 },\n\t{ r: 0xad, g: 0x8e, b: 0xe6 },\n];\n\nconst COLORS = {\n\tborder: { r: 0x56, g: 0x5f, b: 0x89 },\n\ttitle: { r: 0xc0, g: 0xca, b: 0xf5 },\n\tmuted: { r: 0x78, g: 0x7c, b: 0x99 },\n\tsubtle: { r: 0x56, g: 0x5f, b: 0x89 },\n\taccent: { r: 0x7a, g: 0xa2, b: 0xf7 },\n};\n\nfunction truncatePlain(input: string, maxWidth: number): string {\n\tif (maxWidth <= 0) return \"\";\n\n\tlet out = \"\";\n\tlet width = 0;\n\n\tfor (const ch of Array.from(input)) {\n\t\tif (width >= maxWidth) break;\n\t\tout += ch;\n\t\twidth += 1;\n\t}\n\n\treturn out;\n}\n\nfunction truncateAnsi(input: string, maxWidth: number, ellipsis = \"\"): string {\n\tif (maxWidth <= 0) return \"\";\n\tif (visibleWidth(input) <= maxWidth) return input;\n\n\tconst ellipsisWidth = visibleWidth(ellipsis);\n\tif (ellipsisWidth >= maxWidth) return truncatePlain(ellipsis, maxWidth);\n\n\tconst targetWidth = maxWidth - ellipsisWidth;\n\tlet i = 0;\n\tlet width = 0;\n\tlet out = \"\";\n\tlet sawAnsi = false;\n\n\twhile (i < input.length && width < targetWidth) {\n\t\tconst rest = input.slice(i);\n\t\tconst ansi = rest.match(ANSI_ESCAPE_AT_START);\n\t\tif (ansi) {\n\t\t\tout += ansi[0];\n\t\t\tsawAnsi = true;\n\t\t\ti += ansi[0].length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst cp = input.codePointAt(i);\n\t\tif (cp == null) break;\n\n\t\tconst ch = String.fromCodePoint(cp);\n\t\tif (width + 1 > targetWidth) break;\n\n\t\tout += ch;\n\t\twidth += 1;\n\t\ti += ch.length;\n\t}\n\n\tif (ellipsis) out += ellipsis;\n\tif (sawAnsi) out += \"\\x1b[0m\";\n\n\treturn out;\n}\n\nfunction color(rgb: RGB, text: string): string {\n\treturn `\\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m${text}\\x1b[0m`;\n}\n\nfunction bold(text: string): string {\n\treturn `\\x1b[1m${text}\\x1b[0m`;\n}\n\nfunction dim(text: string): string {\n\treturn `\\x1b[38;2;${COLORS.muted.r};${COLORS.muted.g};${COLORS.muted.b}m${text}\\x1b[0m`;\n}\n\nfunction subtle(text: string): string {\n\treturn `\\x1b[38;2;${COLORS.subtle.r};${COLORS.subtle.g};${COLORS.subtle.b}m${text}\\x1b[0m`;\n}\n\nfunction interpolateStops(stops: RGB[], t: number): RGB {\n\tif (stops.length === 0) return { r: 0, g: 0, b: 0 };\n\tif (stops.length === 1) return stops[0];\n\n\tconst clamped = Math.max(0, Math.min(1, t));\n\tconst scaled = clamped * (stops.length - 1);\n\tconst i = Math.min(Math.floor(scaled), stops.length - 2);\n\tconst f = scaled - i;\n\n\treturn {\n\t\tr: Math.round(stops[i].r + (stops[i + 1].r - stops[i].r) * f),\n\t\tg: Math.round(stops[i].g + (stops[i + 1].g - stops[i].g) * f),\n\t\tb: Math.round(stops[i].b + (stops[i + 1].b - stops[i].b) * f),\n\t};\n}\n\nfunction renderColoredLogoLine(line: string): string {\n\tconst chars = Array.from(line);\n\tconst width = chars.length;\n\n\tlet out = \"\";\n\tchars.forEach((ch, i) => {\n\t\tif (ch === \" \") {\n\t\t\tout += \" \";\n\t\t\treturn;\n\t\t}\n\t\tconst t = width > 1 ? i / (width - 1) : 0;\n\t\tout += color(interpolateStops(GRADIENT_STOPS, t), ch);\n\t});\n\n\treturn out;\n}\n\nfunction compactPath(input: string, maxWidth = 44): string {\n\tconst home = os.homedir();\n\tlet normalized = input;\n\tif (normalized.startsWith(home)) {\n\t\tnormalized = `~${normalized.slice(home.length)}`;\n\t}\n\tnormalized = normalized.replaceAll(\"/\", process.platform === \"win32\" ? \"\\\\\" : \"/\");\n\tif (normalized.length <= maxWidth) return normalized;\n\n\tconst separator = normalized.includes(\"\\\\\") ? \"\\\\\" : \"/\";\n\tconst parts = normalized.split(separator).filter(Boolean);\n\tif (parts.length <= 2) return normalized;\n\n\tconst first = normalized.startsWith(separator) ? separator : \"\";\n\tconst isHome = parts[0] === \"~\";\n\tconst driveMatch = parts[0]?.match(/^[A-Za-z]:$/);\n\n\tconst head = isHome ? \"~\" : driveMatch ? `${parts[0]}${separator}` : first + parts[0];\n\tconst tail = parts.slice(-2).join(separator);\n\n\treturn `${head}${separator}…${separator}${tail}`;\n}\n\nfunction padAnsi(input: string, width: number): string {\n\tconst remaining = Math.max(0, width - visibleWidth(input));\n\treturn input + \" \".repeat(remaining);\n}\n\nfunction bulletJoin(parts: Array<string | undefined>): string {\n\treturn parts.filter((part) => Boolean(part && part.trim().length > 0)).join(` ${subtle(\"•\")} `);\n}\n\nfunction maybePrefix(label: string, value: string | undefined): string | undefined {\n\tif (!value) return undefined;\n\treturn `${dim(label)} ${value}`;\n}\n\nexport class Header implements Component {\n\tconstructor(\n\t\tprivate readonly agentSession?: AgentSession,\n\t\tprivate readonly footerDataProvider?: ReadonlyFooterDataProvider,\n\t) {}\n\n\tprivate getData() {\n\t\tconst host = `${os.userInfo().username}@${os.hostname()}`;\n\t\tconst cwd = process.cwd();\n\t\tconst repo = this.footerDataProvider?.getGitRepoName() ?? undefined;\n\t\tconst branch = this.footerDataProvider?.getGitBranch() ?? undefined;\n\t\tconst workspace = this.agentSession?.sessionName;\n\n\t\treturn {\n\t\t\tbranch,\n\t\t\tcwd,\n\t\t\thost,\n\t\t\trepo,\n\t\t\tworkspace,\n\t\t};\n\t}\n\n\tprivate renderCompact(width: number): string[] {\n\t\tconst { branch, cwd, host, repo, workspace } = this.getData();\n\n\t\tconst line1 = truncateAnsi(`${bold(color(COLORS.title, TITLE))} ${dim(VERSION_LABEL)}`, width);\n\t\tconst line2 = subtle(truncateAnsi(bulletJoin([maybePrefix(\"repo\", repo), maybePrefix(\"branch\", branch)]), width));\n\t\tconst line3 = subtle(\n\t\t\ttruncateAnsi(bulletJoin([maybePrefix(\"workspace\", workspace), maybePrefix(\"host\", host)]), width),\n\t\t);\n\t\tconst line4 = subtle(truncateAnsi(maybePrefix(\"cwd\", compactPath(cwd, Math.max(16, width - 8))) ?? \"\", width));\n\n\t\treturn [line1, line2, line3, line4];\n\t}\n\n\trender(width: number): string[] {\n\t\tif (width <= 0) return [\"\"];\n\t\tif (width < 72) return this.renderCompact(width);\n\n\t\tconst { branch, cwd, host, repo, workspace } = this.getData();\n\t\tconst logoWidth = visibleWidth(LOGO[0]);\n\t\tconst gap = 2;\n\t\tconst sidePadding = 1;\n\t\tconst minTextWidth = 12;\n\t\tconst maxTextWidth = Math.max(minTextWidth, width - logoWidth - gap - sidePadding * 2 - 2);\n\n\t\tconst baseRows = [\n\t\t\t`${bold(color(COLORS.title, TITLE))} ${dim(VERSION_LABEL)}`,\n\t\t\tcolor(COLORS.title, bulletJoin([maybePrefix(\"repo\", repo), maybePrefix(\"branch\", branch)])),\n\t\t\tcolor(COLORS.title, bulletJoin([maybePrefix(\"workspace\", workspace), maybePrefix(\"host\", host)])),\n\t\t\t\"\",\n\t\t];\n\n\t\tconst textRows = [\n\t\t\t...baseRows,\n\t\t\tcolor(COLORS.title, maybePrefix(\"cwd\", compactPath(cwd, Math.max(18, maxTextWidth - 10))) ?? \"\"),\n\t\t\t\"\",\n\t\t];\n\t\tconst textWidth = Math.max(\n\t\t\tminTextWidth,\n\t\t\tMath.min(\n\t\t\t\tmaxTextWidth,\n\t\t\t\ttextRows.reduce((max, row) => Math.max(max, visibleWidth(row)), 0),\n\t\t\t),\n\t\t);\n\t\tconst innerWidth = logoWidth + gap + textWidth + sidePadding * 2;\n\n\t\tconst topBorder =\n\t\t\tcolor(COLORS.border, \"╭\") + color(COLORS.border, \"─\".repeat(innerWidth)) + color(COLORS.border, \"╮\");\n\n\t\tconst bottomBorder =\n\t\t\tcolor(COLORS.border, \"╰\") + color(COLORS.border, \"─\".repeat(innerWidth)) + color(COLORS.border, \"╯\");\n\n\t\tconst lines: string[] = [];\n\n\t\t// lines.push(\"\");\n\t\tlines.push(topBorder);\n\t\tfor (let i = 0; i < LOGO.length; i += 1) {\n\t\t\tconst logo = renderColoredLogoLine(LOGO[i]);\n\t\t\tconst text = padAnsi(truncateAnsi(textRows[i] ?? \"\", textWidth), textWidth);\n\n\t\t\tlines.push(`${color(COLORS.border, \"│\")} ${logo}${\" \".repeat(gap)}${text} ${color(COLORS.border, \"│\")}`);\n\t\t}\n\t\tlines.push(bottomBorder);\n\n\t\t// lines.push(\"\");\n\t\treturn lines.map((line) => truncateToWidth(line, width));\n\t}\n\n\tinvalidate(): void {}\n}\n\nexport default Header;\n"]}