fix(tui): use available terminal width for session name display (#16109) (#16238)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 19c18977e0
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
This commit is contained in:
Robby
2026-02-14 18:39:05 +01:00
committed by GitHub
parent 8e5689a84d
commit 5a313c83b7
3 changed files with 33 additions and 14 deletions

View File

@@ -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. - 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. - 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. - 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/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/<email>` allowlists (treat `users/...` as immutable user id only); keep raw email allowlists for usability. Thanks @vincentkoc. - Security/Google Chat: deprecate `users/<email>` 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. - Security/Google Chat: reject ambiguous shared-path webhook routing when multiple webhook targets verify successfully (prevents cross-account policy-context misrouting). Thanks @vincentkoc.

View File

@@ -38,6 +38,16 @@ describe("SearchableSelectList", () => {
expect(output[0]).toContain("search"); 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", () => { it("filters items when typing", () => {
const list = new SearchableSelectList(testItems, 5, mockTheme); const list = new SearchableSelectList(testItems, 5, mockTheme);

View File

@@ -219,20 +219,28 @@ export class SearchableSelectList implements Component {
const displayValue = this.getItemLabel(item); const displayValue = this.getItemLabel(item);
if (item.description && width > 40) { if (item.description && width > 40) {
const maxValueWidth = Math.min(30, width - prefixWidth - 4); const minDescriptionWidth = 12;
const truncatedValue = truncateToWidth(displayValue, maxValueWidth, ""); const spacingWidth = 2;
const valueText = this.highlightMatch(truncatedValue, query); const availableWidth = Math.max(1, width - prefixWidth - 2);
const spacingWidth = Math.max(1, 32 - visibleWidth(valueText));
const spacing = " ".repeat(spacingWidth); if (availableWidth > minDescriptionWidth + spacingWidth + 1) {
const descriptionStart = prefixWidth + visibleWidth(valueText) + spacing.length; const maxValueWidth = availableWidth - minDescriptionWidth - spacingWidth;
const remainingWidth = width - descriptionStart - 2; const truncatedValue = truncateToWidth(displayValue, maxValueWidth, "");
if (remainingWidth > 10) { const valueText = this.highlightMatch(truncatedValue, query);
const truncatedDesc = truncateToWidth(item.description, remainingWidth, "");
// Highlight plain text first, then apply theme styling to avoid corrupting ANSI codes const usedByValue = visibleWidth(valueText);
const highlightedDesc = this.highlightMatch(truncatedDesc, query); const remainingWidth = availableWidth - usedByValue;
const descText = isSelected ? highlightedDesc : this.theme.description(highlightedDesc);
const line = `${prefix}${valueText}${spacing}${descText}`; if (remainingWidth > spacingWidth + 1) {
return isSelected ? this.theme.selectedText(line) : line; 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;
}
} }
} }