Skip to content

feat(tui): add 'open' border mode to TerminalStyle#6368

Merged
jeremymcs merged 11 commits into
gsd-build:mainfrom
jeremymcs:tui-p2/01-open-border-mode
May 18, 2026
Merged

feat(tui): add 'open' border mode to TerminalStyle#6368
jeremymcs merged 11 commits into
gsd-build:mainfrom
jeremymcs:tui-p2/01-open-border-mode

Conversation

@jeremymcs

@jeremymcs jeremymcs commented May 18, 2026

Copy link
Copy Markdown
Contributor

TUI Phase 2 — migration step 1/7

Implements step 1 of the ADR-019 migration plan (#6367). Stacked on the ADR branch.

What

Adds an open border mode to TerminalStyle — the copy-clean content surface the ADR calls for:

  • Titled top rule─── bash · success ─────────── 1.2s ───, via a new renderOpenTopRule helper. Supports left title, right title, or neither.
  • Body lines emitted verbatim — no vertical border column, no prefix. Selecting a body line in the terminal copies only its content.
  • Plain closing rule.

Why additive-only

No existing component uses open yet — this PR just lands the primitive. Steps 2–7 migrate consumers onto it.

Tests

3 new tests in style.test.ts, including an explicit assertion that no body line starts with a border glyph ( / / ) — the copy-cleanliness contract. Full style suite: 9/9 pass. tsc clean.

Summary by CodeRabbit

  • New Features

    • Added "open" border style for cleaner display
    • Added color quantization support for limited-color terminals
    • Added text truncation in selection lists
  • Bug Fixes

    • Fixed input deletion logic edge case
    • Fixed overlay dismissal not redrawing properly
    • Fixed Unicode and emoji character handling in search
    • Fixed filtering behavior when no results match
  • Improvements

    • Enhanced component lifecycle cleanup management
    • Improved markdown list rendering structure
  • Documentation

    • Added TUI style system architecture decision record

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b33a1fdd-7831-44f5-a0fc-2166efbbd0a1

📥 Commits

Reviewing files that changed from the base of the PR and between 4fa36b7 and 21277ed.

📒 Files selected for processing (16)
  • docs/dev/ADR-019-unify-tui-style-system.md
  • packages/pi-coding-agent/src/modes/interactive/components/armin.ts
  • packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts
  • packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts
  • packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts
  • packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts
  • packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts
  • packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts
  • packages/pi-tui/src/__tests__/style.test.ts
  • packages/pi-tui/src/__tests__/tui.test.ts
  • packages/pi-tui/src/components/input.ts
  • packages/pi-tui/src/components/markdown.ts
  • packages/pi-tui/src/components/select-list.ts
  • packages/pi-tui/src/fuzzy.ts
  • packages/pi-tui/src/style.ts
  • packages/pi-tui/src/tui.ts
🚧 Files skipped from review as they are similar to previous changes (14)
  • packages/pi-coding-agent/src/modes/interactive/components/armin.ts
  • packages/pi-tui/src/fuzzy.ts
  • packages/pi-tui/src/tests/style.test.ts
  • packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts
  • packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts
  • packages/pi-tui/src/components/input.ts
  • packages/pi-tui/src/components/markdown.ts
  • packages/pi-tui/src/tui.ts
  • packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts
  • packages/pi-tui/src/components/select-list.ts
  • packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts
  • packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts
  • packages/pi-tui/src/style.ts
  • packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts

📝 Walkthrough

Walkthrough

This PR implements Phase 0 of the TUI audit roadmap, introducing a copy-clean "open" border style, fixing render-state caching, preventing event-loop blocking via resource cleanup, and fixing bugs in text handling, selection, and fuzzy matching across the pi-tui and pi-coding-agent packages.

Changes

TUI Audit Phase 0 Implementation

Layer / File(s) Summary
ADR & Audit Documentation
docs/dev/ADR-019-unify-tui-style-system.md
ADR-019 defines the vision for copy-clean TUI surfaces via a new "open" border mode, consolidates border rendering into TerminalStyle, and establishes two role-based surface types ("Open" for content, "Panel" for interactive UI). Includes phased implementation plan and verification steps.
TUI Style System: Open Border Mode
packages/pi-tui/src/style.ts, packages/pi-tui/src/__tests__/style.test.ts
TerminalBorderStyle gains an "open" option. render() treats "open" like "none" for vertical borders, adds a dedicated "open" branch emitting a titled top rule, copy-clean padded body lines, and a closing rule. New renderOpenTopRule() helper constructs rule lines with optional titles. Tests verify copy-clean body lines, plain dash rules, and correct width handling.
TUI Render Loop: Overlay State Tracking
packages/pi-tui/src/tui.ts, packages/pi-tui/src/__tests__/tui.test.ts
Adds _lastFrameHadOverlays flag to track overlay presence across frames. doRender() now skips rendering only when base output is unchanged, there are no overlays, and the prior frame had none—ensuring dismissed overlays are erased even when content is byte-identical. Regression test confirms redraw after overlay dismissal.
Component Lifecycle & Resource Cleanup
packages/pi-coding-agent/src/modes/interactive/components/armin.ts, countdown-timer.ts, dynamic-border.ts, session-selector.ts, interactive-mode.ts, daxnuts.ts
Prevents timers and watchers from pinning the Node.js event loop. armin.ts and countdown-timer.ts call unref() on intervals; dynamic-border.ts and session-selector.ts implement dispose() to clean up spinners and timeouts; interactive-mode.ts captures and disposes the theme-change watcher; daxnuts.ts detects terminal color mode for capability-aware escape codes.
Rendering & Selection Logic Improvements
packages/pi-tui/src/components/select-list.ts, markdown.ts, input.ts, fuzzy.ts, packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts
select-list.ts guards against navigation when filter yields no items. markdown.ts refactors nested-list detection from regex to structural ListItemLine tracking. input.ts clamps yank-pop deletion to avoid negative indices. fuzzy.ts switches to Unicode code-point iteration for correct emoji handling. model-selector.ts uses TruncatedText for grouped lists to prevent layout breakage.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • gsd-build/gsd-2#6118: Both PRs add unref() calls to TUI animation timers and implement lifecycle cleanup methods to prevent event-loop blocking.
  • gsd-build/gsd-2#6186: Both PRs modify packages/pi-tui/src/tui.ts render behavior and early-exit conditions in doRender().
  • gsd-build/gsd-2#6131: Both PRs modify doRender() render/early-exit logic to change redraw behavior under different conditions.

Suggested labels

needs-review, test

Poem

🐰 The TUI borders now breathe clean,
No railings block what's to be seen—
Copy-clean surfaces frame the way,
Timers unref to not overstay,
Code points counted, lists track true,
Phase Zero renders bold and new!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(tui): add 'open' border mode to TerminalStyle' directly and clearly summarizes the main feature addition—the new 'open' border mode for TerminalStyle, which is the core objective of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🟠 PR Risk Report — HIGH

Files changed 16
Systems affected 4
Overall risk 🟠 HIGH

Affected Systems

Risk System
🟠 high Session Management
🟠 high Modes
🟡 medium TUI Components
🟡 medium Model System
File Breakdown
Risk File Systems
🟡 packages/pi-coding-agent/src/modes/interactive/components/armin.ts TUI Components
🟡 packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts TUI Components
🟡 packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts TUI Components
🟡 packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts TUI Components
🟡 packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts TUI Components, Model System
🟠 packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts TUI Components, Session Management
🟠 packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts Modes, TUI Components
🟡 packages/pi-tui/src/components/input.ts TUI Components
🟡 packages/pi-tui/src/components/markdown.ts TUI Components
🟡 packages/pi-tui/src/components/select-list.ts TUI Components
🟡 packages/pi-tui/src/fuzzy.ts TUI Components
🟡 packages/pi-tui/src/tui.ts TUI Components
docs/dev/ADR-019-unify-tui-style-system.md (unclassified)
packages/pi-tui/src/__tests__/style.test.ts (unclassified)
packages/pi-tui/src/__tests__/tui.test.ts (unclassified)
packages/pi-tui/src/style.ts (unclassified)

⚠️ 🟠 High risk — the following systems require verification before merge:

  • 🟠 Session Management: test session creation, restoration, and teardown
  • 🟠 Modes: test mode switching transitions and state isolation

⛔ This PR should not be merged without executing this follow-up prompt.

Ask your coding agent to verify before submitting:

Review this PR for risks in: Session Management, Modes. Verify:

1. test session creation, restoration, and teardown
2. test mode switching transitions and state isolation

Before modifying any code, assess the scope of this fix:

- Identify the root cause, not just the reported symptom.
- Search the codebase for other call sites, similar patterns, or duplicated logic that may share the same bug.
- List affected tests, documentation, and any downstream consumers that depend on the current behavior.
- Flag any changes that extend beyond the immediate file or function.

Report findings first. Then propose a fix scoped to the actual root cause, and wait for confirmation before applying changes outside the originally reported location.

💡 Have a Codex subscription? Get an independent second opinion: codex review --adversarial

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/pi-tui/src/__tests__/style.test.ts (1)

106-117: ⚡ Quick win

Add a long-title regression for open rule width clamping.

This test only exercises a width where both titles fit. Add one case with overlong left/right titles and assert visibleWidth(plain[0]) still equals the requested width to prevent overflow regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pi-tui/src/__tests__/style.test.ts` around lines 106 - 117, Add a
regression case to the existing test that verifies width clamping when titles
are too long: in the "open border places left and right titles in the top rule"
test, after the current assertions, call
style().border("open").title(longLeftTitle).titleRight(longRightTitle).render(["output"],
40).map(stripAnsi) using deliberately overlong strings for longLeftTitle and
longRightTitle, then assert visibleWidth(plain[0]) === 40 (and optionally that
the rendered line still contains truncated/partial title fragments) to ensure
the open-rule top border clamps to the requested width rather than overflowing.
packages/pi-tui/src/tui.ts (1)

663-667: ⚡ Quick win

Track visible overlays (not stack length) in the fast-path gate.

The new gate is intended to model “was an overlay composited last frame”, but Line 665 / Line 671 use this.overlayStack.length > 0. Hidden or non-visible overlays keep the optimization disabled unnecessarily.

♻️ Proposed adjustment
 		// Render all components to get new lines
 		let newLines = this.render(width);
+		const hasVisibleOverlays = this.hasOverlay();

 		if (
 			newLines === this._lastRenderedComponents &&
-			this.overlayStack.length === 0 &&
+			!hasVisibleOverlays &&
 			!this._lastFrameHadOverlays
 		) {
 			return;
 		}
 		this._lastRenderedComponents = newLines;
-		this._lastFrameHadOverlays = this.overlayStack.length > 0;
+		this._lastFrameHadOverlays = hasVisibleOverlays;

 		// Composite overlays into the rendered lines (before differential compare)
-		if (this.overlayStack.length > 0) {
+		if (hasVisibleOverlays) {
 			newLines = compositeOverlays(newLines, this.overlayStack, width, height, this.maxLinesRendered);
 		}

Also applies to: 671-676

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pi-tui/src/tui.ts` around lines 663 - 667, The fast-path gate
currently checks overlayStack.length to decide if an overlay was composited,
which is wrong for hidden/non-visible overlays; replace those length checks with
a computed boolean that tests whether any overlay in overlayStack is actually
visible/composited (e.g., const currentFrameHasVisibleOverlays =
overlayStack.some(o => o.visible /* or o.isVisible / o.shouldComposite as
appropriate */)), use currentFrameHasVisibleOverlays in the two conditional
expressions (where overlayStack.length > 0 is used), and set
_lastFrameHadOverlays = currentFrameHasVisibleOverlays instead of using the
stack length.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/pi-tui/src/style.ts`:
- Around line 227-253: The renderOpenTopRule function can produce output wider
than the requested width when titles are long because fill is forced to at least
1 and titles are never truncated; fix it by clamping/truncating title text to
available space before computing fill (use visibleWidth to measure and a
truncation helper to shorten left/right titles while preserving color via
color()), compute fill = Math.max(0, w - fixed) instead of min 1, and recalc
fixed after truncation so the concatenated pieces always equal the requested
width; update the branches in renderOpenTopRule to truncate left/right (or both)
as needed and only then build the final string.

---

Nitpick comments:
In `@packages/pi-tui/src/__tests__/style.test.ts`:
- Around line 106-117: Add a regression case to the existing test that verifies
width clamping when titles are too long: in the "open border places left and
right titles in the top rule" test, after the current assertions, call
style().border("open").title(longLeftTitle).titleRight(longRightTitle).render(["output"],
40).map(stripAnsi) using deliberately overlong strings for longLeftTitle and
longRightTitle, then assert visibleWidth(plain[0]) === 40 (and optionally that
the rendered line still contains truncated/partial title fragments) to ensure
the open-rule top border clamps to the requested width rather than overflowing.

In `@packages/pi-tui/src/tui.ts`:
- Around line 663-667: The fast-path gate currently checks overlayStack.length
to decide if an overlay was composited, which is wrong for hidden/non-visible
overlays; replace those length checks with a computed boolean that tests whether
any overlay in overlayStack is actually visible/composited (e.g., const
currentFrameHasVisibleOverlays = overlayStack.some(o => o.visible /* or
o.isVisible / o.shouldComposite as appropriate */)), use
currentFrameHasVisibleOverlays in the two conditional expressions (where
overlayStack.length > 0 is used), and set _lastFrameHadOverlays =
currentFrameHasVisibleOverlays instead of using the stack length.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4fb93238-368d-49dc-b8fa-ff46d57c2d87

📥 Commits

Reviewing files that changed from the base of the PR and between a72b3a0 and 4fa36b7.

📒 Files selected for processing (16)
  • docs/adr/0001-unify-tui-style-system.md
  • docs/tui-audit.md
  • packages/pi-coding-agent/src/modes/interactive/components/armin.ts
  • packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts
  • packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts
  • packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts
  • packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts
  • packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts
  • packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts
  • packages/pi-tui/src/__tests__/style.test.ts
  • packages/pi-tui/src/components/input.ts
  • packages/pi-tui/src/components/markdown.ts
  • packages/pi-tui/src/components/select-list.ts
  • packages/pi-tui/src/fuzzy.ts
  • packages/pi-tui/src/style.ts
  • packages/pi-tui/src/tui.ts

Comment on lines +227 to +253
private renderOpenTopRule(width: number, borderColor: (text: string) => string): string {
const w = Math.max(1, width);
const left = this.spec.title ?? "";
const right = this.spec.titleRight ?? "";
if (!left && !right) return borderColor("─".repeat(w));

const styledLeft = color(this.spec.titleColor, left);
const styledRight = color(this.spec.titleRightColor, right);

if (left && right) {
const fixed = 4 + visibleWidth(left) + 2 + visibleWidth(right) + 4;
const fill = Math.max(1, w - fixed);
return (
borderColor("─── ") +
styledLeft +
borderColor(` ${"─".repeat(fill)} `) +
styledRight +
borderColor(" ───")
);
}
if (left) {
const fill = Math.max(1, w - 5 - visibleWidth(left));
return borderColor("─── ") + styledLeft + borderColor(` ${"─".repeat(fill)}`);
}
const fill = Math.max(1, w - 5 - visibleWidth(right));
return borderColor(`${"─".repeat(fill)} `) + styledRight + borderColor(" ───");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clamp open-rule titles to preserve exact line width.

Line 237 through Line 252 can generate lines wider than width when titles are long (titles are never truncated, and fill is forced to at least 1). That can wrap and break the surface contract.

💡 Proposed fix
 private renderOpenTopRule(width: number, borderColor: (text: string) => string): string {
 	const w = Math.max(1, width);
-	const left = this.spec.title ?? "";
-	const right = this.spec.titleRight ?? "";
+	let left = this.spec.title ?? "";
+	let right = this.spec.titleRight ?? "";
 	if (!left && !right) return borderColor("─".repeat(w));
+	if (w < 8) return borderColor("─".repeat(w));

-	const styledLeft = color(this.spec.titleColor, left);
-	const styledRight = color(this.spec.titleRightColor, right);
+	if (left && right) {
+		const titleBudget = Math.max(1, w - 11); // decorations + at least 1 fill rune
+		right = truncateToWidth(right, Math.max(1, Math.floor(titleBudget / 2)), "");
+		left = truncateToWidth(left, Math.max(1, titleBudget - visibleWidth(right)), "");
+	} else {
+		const singleBudget = Math.max(1, w - 6);
+		left = truncateToWidth(left, singleBudget, "");
+		right = truncateToWidth(right, singleBudget, "");
+	}
+
+	const styledLeft = color(this.spec.titleColor, left);
+	const styledRight = color(this.spec.titleRightColor, right);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pi-tui/src/style.ts` around lines 227 - 253, The renderOpenTopRule
function can produce output wider than the requested width when titles are long
because fill is forced to at least 1 and titles are never truncated; fix it by
clamping/truncating title text to available space before computing fill (use
visibleWidth to measure and a truncation helper to shorten left/right titles
while preserving color via color()), compute fill = Math.max(0, w - fixed)
instead of min 1, and recalc fixed after truncation so the concatenated pieces
always equal the requested width; update the branches in renderOpenTopRule to
truncate left/right (or both) as needed and only then build the final string.

jeremymcs added 9 commits May 18, 2026 08:45
The doRender() short-circuit skipped all post-processing when component
output was byte-identical to the previous frame. If the previous frame
composited an overlay onto the screen, an identical next frame with no
overlay would short-circuit and never erase it, leaving the overlay
stuck on screen.

Track _lastFrameHadOverlays and force a redraw when the previous frame
drew an overlay, even if base component output is unchanged.
When a filter matched no items, selectUp set selectedIndex to
filteredItems.length - 1 (i.e. -1), corrupting list state until the
next setFilter() reset it. Skip navigation and selection entirely when
the filtered list is empty; still honor cancel.
renderList() identified nested-list lines with the regex
/^\s+\x1b\[36m[-\d]/ — assuming the list bullet is always ANSI cyan.
Any theme with a different bullet color silently broke nested-list
indentation.

renderListItem() now returns ListItemLine[] with a 'nested' flag set on
lines produced by recursive renderList() calls, so renderList() keys off
structure instead of pattern-matching theme output.
yankPop() computed the deletion start as cursor - prevText.length with
no floor. A negative index passed to String.slice() counts from the end
of the string, so an underflow would silently corrupt the input value
rather than delete nothing. Clamp the start index to 0.
fuzzyMatch() indexed strings with [i], which yields UTF-16 code units.
Astral-plane characters (emoji, rare CJK) occupy two code units, so a
single such character was compared as two lone surrogates and never
matched. Iterate over Array.from() code-point arrays instead.
The daxnuts easter-egg image emitted 24-bit truecolor SGR sequences
(\x1b[38;2;r;g;bm) unconditionally. On 256/16-color terminals (many
containers, remote sessions, Apple Terminal) those render incorrectly.

Add an rgbTo256() quantizer; rgb() now emits 256-color SGR when
theme.getColorMode() reports the terminal is not truecolor.
The shutdown path called onThemeChange(() => {}), which registered a
new empty listener instead of removing the one added at startup,
leaking theme-change subscribers. Store the unsubscriber returned by
onThemeChange() and invoke it during cleanup.
Timer-hygiene pass over animated/timed TUI components:

- DynamicBorder: add dispose() that stops the spinner. Without it, a
  spinner started via startSpinner() keeps firing its interval and
  calling ui.requestRender() after the border is detached.
- SessionSelectorHeader: add dispose() that clears the status-message
  auto-hide timeout, so it cannot fire requestRender() post-removal.
- CountdownTimer / Armin: unref() the interval so a cosmetic timer
  never pins the Node event loop on its own if dispose() is missed.

terminal.ts:199 was also flagged but is correct as-is — the fallback
setTimeout body is guarded by !_kittyProtocolActive, so it is a no-op
when the protocol is confirmed before the 150ms deadline.
Provider-header and model rows were built with Text, which word-wraps.
A model id longer than the overlay width wrapped onto extra lines,
pushing rows out of the fixed 12-row selection window and leaving the
selection cursor on a different line from the rest of the row.

Use TruncatedText for header and model rows so an over-long id is
truncated to one line. The multi-line 'No providers' help text keeps
using Text intentionally — it should wrap.
@jeremymcs jeremymcs force-pushed the tui-p2/01-open-border-mode branch from 4fa36b7 to 8e38045 Compare May 18, 2026 13:47
jeremymcs added 2 commits May 18, 2026 09:03
ADR-019. Two converging problems: (1) users cannot cleanly copy TUI
output because every content line carries a border/rail prefix that the
terminal copies with the text; (2) four independent border
implementations and three message container styles.

Decision: content surfaces (message turns, tool/bash output, summaries)
render copy-clean with horizontal-rule framing and zero leading
characters on body lines. One border primitive (TerminalStyle, with a
new 'open' mode); one app vocabulary module; two surfaces by role —
Open (content, copy-clean) and Panel (interactive, bordered). Rail and
box-background styles eliminated. Seven-step stacked-PR migration plan.

Status: Accepted.
First step of the ADR-019 style-system migration. Adds an 'open'
border mode for copy-clean content surfaces:

- A titled top rule (── bash · success ──── 1.2s ──) via the new
  renderOpenTopRule helper.
- Body lines emitted verbatim with no border column and no prefix, so
  selecting a body line in the terminal copies only its content.
- A plain closing rule.

Purely additive — no existing consumer uses 'open' yet. Unit tests
assert body lines never gain a leading border glyph.
@jeremymcs jeremymcs force-pushed the tui-p2/01-open-border-mode branch from 8e38045 to 21277ed Compare May 18, 2026 14:11
@jeremymcs jeremymcs merged commit 21277ed into gsd-build:main May 18, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant