{"version":3,"file":"format-list.d.ts","sourceRoot":"","sources":["../../src/core/format-list.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,uEAAuE;AACvE,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED,4DAA4D;AAC5D,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,SAAM,GAAG,MAAM,CAgBtF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAsBnF;AAED,kCAAkC;AAClC,MAAM,WAAW,OAAO;IACvB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,sFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,+DAA+D;AAC/D,MAAM,WAAW,SAAS;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,EAAE,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,MAAM,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,MAAM,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,OAAO,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,iBAAiB;IACjC,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAID;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAuDvF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAChC,IAAI,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,EAC7C,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAA;CAAO,GAC7F,MAAM,CAiBR","sourcesContent":["/**\n * One row shape for every capability listing.\n *\n * Plugins, marketplaces, agents and skills are listed in at least five places —\n * the `/plugin` commands, the startup resource summary, and the model-facing\n * `SearchPlugins`/`ListPlugins` tools. Each had grown its own `name [a, b] — desc`\n * variant, so the same plugin printed three different ways depending on which\n * surface you asked. This module owns the layout so they cannot drift again.\n *\n * Two constraints shape the API:\n *\n * 1. **Styling is injected, never assumed.** The same rows are rendered as plain\n *    text (tool results the model reads, RPC payloads a client formats itself)\n *    and as themed terminal output. Callers pass a {@link ListStyle}; the default\n *    is identity, so the plain path costs nothing and can never emit an escape\n *    code into a protocol message.\n * 2. **Width is optional.** Only the interactive surface knows the terminal\n *    width. With `columns` set, detail text is wrapped and indented under its\n *    row; without it, detail is emitted unwrapped and the consumer wraps. The\n *    TUI's own wrapper has no hanging indent, which is what made the old\n *    listings unreadable — a wrapped description restarted at column 0 and read\n *    as a new entry.\n */\n\nimport { visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport { SEGMENT_SEP } from \"./brand.js\";\n\n/** `1 plugin` / `2 plugins`, so listings stop printing `plugin(s)`. */\nexport function plural(count: number, word: string, pluralForm?: string): string {\n\treturn `${count} ${count === 1 ? word : (pluralForm ?? `${word}s`)}`;\n}\n\n/** Pad to `width` display columns, measuring ANSI-aware. */\nexport function padCell(text: string, width: number): string {\n\treturn text + \" \".repeat(Math.max(0, width - visibleWidth(text)));\n}\n\n/**\n * Truncate to `maxWidth` display columns.\n *\n * Deliberately not the TUI's `truncateToWidth`, which wraps its result in\n * `\\x1b[0m` even for plain input. That would put escape codes into text the\n * model and RPC clients read, and a *full* reset at that — dropping any style\n * the caller had applied around it, mid-line.\n */\nexport function truncateVisible(text: string, maxWidth: number, ellipsis = \"…\"): string {\n\tif (maxWidth <= 0) return \"\";\n\tif (visibleWidth(text) <= maxWidth) return text;\n\n\tconst budget = maxWidth - visibleWidth(ellipsis);\n\tif (budget <= 0) return ellipsis;\n\n\tlet out = \"\";\n\tlet width = 0;\n\tfor (const char of text) {\n\t\tconst charWidth = visibleWidth(char);\n\t\tif (width + charWidth > budget) break;\n\t\tout += char;\n\t\twidth += charWidth;\n\t}\n\treturn `${out}${ellipsis}`;\n}\n\n/**\n * Wrap `text` to `width`, indenting every line by `indent` spaces — including\n * the continuations, which is the whole point (see the module note).\n */\nexport function wrapIndented(text: string, indent: number, width?: number): string[] {\n\tconst prefix = \" \".repeat(indent);\n\tif (width === undefined) return [`${prefix}${text}`];\n\n\tconst budget = Math.max(8, width - indent);\n\tconst words = text.split(/\\s+/).filter((w) => w.length > 0);\n\tif (words.length === 0) return [];\n\n\tconst lines: string[] = [];\n\tlet current = \"\";\n\tfor (const word of words) {\n\t\tif (current === \"\") {\n\t\t\tcurrent = word;\n\t\t} else if (visibleWidth(current) + 1 + visibleWidth(word) <= budget) {\n\t\t\tcurrent += ` ${word}`;\n\t\t} else {\n\t\t\tlines.push(`${prefix}${current}`);\n\t\t\tcurrent = word;\n\t\t}\n\t}\n\tif (current) lines.push(`${prefix}${current}`);\n\treturn lines;\n}\n\n/** A single listed capability. */\nexport interface ListRow {\n\t/** Primary identifier. Every row in the listing aligns on this column. */\n\tname: string;\n\t/** Marker before the name — e.g. an installed tick. Counted in the column width. */\n\tmarker?: string;\n\t/** Short facts beside the name (platforms, capabilities, source kind). */\n\tfacts?: string[];\n\t/** Free text on its own wrapped, indented line(s) below the row. */\n\tdetail?: string;\n\t/** Extra indented line below the detail — provenance, a path, a URL. */\n\ttrailer?: string;\n}\n\n/** Rows under an optional heading (a marketplace, a scope). */\nexport interface ListGroup {\n\ttitle?: string;\n\trows: ListRow[];\n}\n\n/**\n * Styling hooks. Every one defaults to identity so the plain-text path emits no\n * escape codes at all — required for tool results and RPC payloads.\n */\nexport interface ListStyle {\n\tname?(text: string): string;\n\tmarker?(text: string): string;\n\tfacts?(text: string): string;\n\tdetail?(text: string): string;\n\ttrailer?(text: string): string;\n\tgroupTitle?(text: string): string;\n}\n\nexport interface RenderListOptions {\n\t/** Terminal width, when the surface knows it. Enables wrapping and truncation. */\n\tcolumns?: number;\n\t/** Columns of indent applied to group titles. Rows sit two further in. */\n\tindent?: number;\n\tstyle?: ListStyle;\n\t/** Separator between facts. Defaults to the shared segment dot. */\n\tfactSeparator?: string;\n}\n\nconst identity = (text: string) => text;\n\n/**\n * Render grouped rows as aligned text.\n *\n * The name column is measured across *every* group, not per group, so the\n * listing reads as one table rather than a stack of independently ragged ones.\n */\nexport function renderList(groups: ListGroup[], options: RenderListOptions = {}): string {\n\tconst style = options.style ?? {};\n\tconst styleName = style.name ?? identity;\n\tconst styleMarker = style.marker ?? identity;\n\tconst styleFacts = style.facts ?? identity;\n\tconst styleDetail = style.detail ?? identity;\n\tconst styleTrailer = style.trailer ?? identity;\n\tconst styleGroupTitle = style.groupTitle ?? identity;\n\tconst factSeparator = options.factSeparator ?? ` ${SEGMENT_SEP} `;\n\n\tconst baseIndent = options.indent ?? 0;\n\tconst hasTitles = groups.some((g) => g.title !== undefined);\n\tconst rowIndent = baseIndent + (hasTitles ? 2 : 0);\n\tconst detailIndent = rowIndent + 4;\n\n\tconst allRows = groups.flatMap((g) => g.rows);\n\tif (allRows.length === 0) return \"\";\n\n\tconst anyMarker = allRows.some((r) => r.marker);\n\tconst markerWidth = anyMarker ? Math.max(...allRows.map((r) => visibleWidth(r.marker ?? \"\"))) + 1 : 0;\n\tconst nameWidth = Math.max(...allRows.map((r) => visibleWidth(r.name))) + markerWidth;\n\n\tconst lines: string[] = [];\n\tfor (const group of groups) {\n\t\tif (group.title !== undefined) {\n\t\t\tlines.push(`${\" \".repeat(baseIndent)}${styleGroupTitle(group.title)}`);\n\t\t}\n\t\tfor (const row of group.rows) {\n\t\t\t// Widths are measured on the plain text and the padding is emitted\n\t\t\t// separately, so styling can never be counted as visible cells.\n\t\t\tconst markerPlain = row.marker ?? \"\";\n\t\t\tconst markerCell = anyMarker\n\t\t\t\t? (row.marker ? styleMarker(row.marker) : \"\") + \" \".repeat(markerWidth - visibleWidth(markerPlain))\n\t\t\t\t: \"\";\n\t\t\tconst namePad = \" \".repeat(Math.max(0, nameWidth - markerWidth - visibleWidth(row.name)));\n\t\t\tconst nameCell = `${markerCell}${styleName(row.name)}${namePad}`;\n\t\t\tconst facts = row.facts?.filter((f) => f.length > 0) ?? [];\n\t\t\tconst factsText = facts.length > 0 ? `  ${styleFacts(facts.join(factSeparator))}` : \"\";\n\t\t\tlines.push(`${\" \".repeat(rowIndent)}${nameCell}${factsText}`.trimEnd());\n\n\t\t\tif (row.detail) {\n\t\t\t\tfor (const line of wrapIndented(row.detail, detailIndent, options.columns)) {\n\t\t\t\t\tlines.push(styleDetail(line));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (row.trailer) {\n\t\t\t\tconst trailer =\n\t\t\t\t\toptions.columns === undefined\n\t\t\t\t\t\t? row.trailer\n\t\t\t\t\t\t: truncateVisible(row.trailer, Math.max(8, options.columns - detailIndent));\n\t\t\t\tlines.push(styleTrailer(`${\" \".repeat(detailIndent)}${trailer}`));\n\t\t\t}\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Single-line variant: name column plus one description, truncated to width\n * rather than wrapped. Used where a listing must stay one row per item (the\n * startup summary, where every extra line pushes the prompt down a page).\n */\nexport function renderCompactRows(\n\trows: Array<{ name: string; detail: string }>,\n\toptions: { columns?: number; indent?: number; style?: Pick<ListStyle, \"name\" | \"detail\"> } = {},\n): string {\n\tif (rows.length === 0) return \"\";\n\tconst indent = options.indent ?? 2;\n\tconst styleName = options.style?.name ?? identity;\n\tconst styleDetail = options.style?.detail ?? identity;\n\tconst nameWidth = Math.max(...rows.map((r) => visibleWidth(r.name)));\n\n\treturn rows\n\t\t.map((row) => {\n\t\t\tconst gap = \" \".repeat(Math.max(0, nameWidth - visibleWidth(row.name)) + 2);\n\t\t\tconst detail =\n\t\t\t\toptions.columns === undefined\n\t\t\t\t\t? row.detail\n\t\t\t\t\t: truncateVisible(row.detail, Math.max(8, options.columns - indent - nameWidth - 2));\n\t\t\treturn `${\" \".repeat(indent)}${styleName(row.name)}${gap}${styleDetail(detail)}`.trimEnd();\n\t\t})\n\t\t.join(\"\\n\");\n}\n"]}