From 5a313c83b7b3871472ba2494ced7c47574f66d88 Mon Sep 17 00:00:00 2001 From: Robby Date: Sat, 14 Feb 2026 18:39:05 +0100 Subject: [PATCH] fix(tui): use available terminal width for session name display (#16109) (#16238) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: 19c18977e0d2350825502d07adfcc00dbde6e073 Co-authored-by: robbyczgw-cla <239660374+robbyczgw-cla@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> Reviewed-by: @steipete --- CHANGELOG.md | 1 + .../components/searchable-select-list.test.ts | 10 ++++++ src/tui/components/searchable-select-list.ts | 36 +++++++++++-------- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9507d21ec25..6d92da77f1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai - Security/Discovery: stop treating Bonjour TXT records as authoritative routing (prefer resolved service endpoints) and prevent discovery from overriding stored TLS pins; autoconnect now requires a previously trusted gateway. Thanks @simecek. - macOS: hard-limit unkeyed `openclaw://agent` deep links and ignore `deliver` / `to` / `channel` unless a valid unattended key is provided. Thanks @Cillian-Collins. - Plugins: suppress false duplicate plugin id warnings when the same extension is discovered via multiple paths (config/workspace/global vs bundled), while still warning on genuine duplicates. (#16222) Thanks @shadril238. +- TUI: use available terminal width for session name display in searchable select lists. (#16238) Thanks @robbyczgw-cla. - Security/Voice Call: require valid Twilio webhook signatures even when ngrok free tier loopback compatibility mode is enabled. Thanks @p80n-sec. - Security/Google Chat: deprecate `users/` allowlists (treat `users/...` as immutable user id only); keep raw email allowlists for usability. Thanks @vincentkoc. - Security/Google Chat: reject ambiguous shared-path webhook routing when multiple webhook targets verify successfully (prevents cross-account policy-context misrouting). Thanks @vincentkoc. diff --git a/src/tui/components/searchable-select-list.test.ts b/src/tui/components/searchable-select-list.test.ts index cf1da265e9b..b618036743f 100644 --- a/src/tui/components/searchable-select-list.test.ts +++ b/src/tui/components/searchable-select-list.test.ts @@ -38,6 +38,16 @@ describe("SearchableSelectList", () => { expect(output[0]).toContain("search"); }); + it("does not truncate long labels on wide terminals when description is present", () => { + const tail = "__TAIL__"; + const longLabel = `session-${"x".repeat(40)}${tail}`; // > 30 chars; tail would be lost before PR + const items = [{ value: longLabel, label: longLabel, description: "desc" }]; + const list = new SearchableSelectList(items, 5, mockTheme); + + const output = list.render(120).join("\n"); + expect(output).toContain(tail); + }); + it("filters items when typing", () => { const list = new SearchableSelectList(testItems, 5, mockTheme); diff --git a/src/tui/components/searchable-select-list.ts b/src/tui/components/searchable-select-list.ts index 6c15e1b403d..6fced886ea0 100644 --- a/src/tui/components/searchable-select-list.ts +++ b/src/tui/components/searchable-select-list.ts @@ -219,20 +219,28 @@ export class SearchableSelectList implements Component { const displayValue = this.getItemLabel(item); if (item.description && width > 40) { - const maxValueWidth = Math.min(30, width - prefixWidth - 4); - const truncatedValue = truncateToWidth(displayValue, maxValueWidth, ""); - const valueText = this.highlightMatch(truncatedValue, query); - const spacingWidth = Math.max(1, 32 - visibleWidth(valueText)); - const spacing = " ".repeat(spacingWidth); - const descriptionStart = prefixWidth + visibleWidth(valueText) + spacing.length; - const remainingWidth = width - descriptionStart - 2; - if (remainingWidth > 10) { - const truncatedDesc = truncateToWidth(item.description, remainingWidth, ""); - // Highlight plain text first, then apply theme styling to avoid corrupting ANSI codes - const highlightedDesc = this.highlightMatch(truncatedDesc, query); - const descText = isSelected ? highlightedDesc : this.theme.description(highlightedDesc); - const line = `${prefix}${valueText}${spacing}${descText}`; - return isSelected ? this.theme.selectedText(line) : line; + const minDescriptionWidth = 12; + const spacingWidth = 2; + const availableWidth = Math.max(1, width - prefixWidth - 2); + + if (availableWidth > minDescriptionWidth + spacingWidth + 1) { + const maxValueWidth = availableWidth - minDescriptionWidth - spacingWidth; + const truncatedValue = truncateToWidth(displayValue, maxValueWidth, ""); + const valueText = this.highlightMatch(truncatedValue, query); + + const usedByValue = visibleWidth(valueText); + const remainingWidth = availableWidth - usedByValue; + + if (remainingWidth > spacingWidth + 1) { + const descriptionWidth = remainingWidth - spacingWidth; + const spacing = " ".repeat(spacingWidth); + const truncatedDesc = truncateToWidth(item.description, descriptionWidth, ""); + // Highlight plain text first, then apply theme styling to avoid corrupting ANSI codes + const highlightedDesc = this.highlightMatch(truncatedDesc, query); + const descText = isSelected ? highlightedDesc : this.theme.description(highlightedDesc); + const line = `${prefix}${valueText}${spacing}${descText}`; + return isSelected ? this.theme.selectedText(line) : line; + } } }