Skip to content

Card copy is measured in code points on unsanitized text, and drawn in cells on sanitized text #71

Description

@bmethod

Source: suppressed findings in the Copilot review of #69, read from the review body at head 353bf23.
Reached by: a lane whose name holds a wide character, which is any worktree whose basename is CJK (src/model/lanes.ts builds the lane name from the workspace basename), and a lane title carrying a control byte from a process environment value.
Expected delta: 120 lines, 160 test lines

Card copy is measured in code points on the raw string, while the terminal draws cells on the sanitized string. Both differences make a detail wrap to more rows than the budget allowed, which pushes Next and the command down the screen. That is the defect #69 exists to remove, reached by two inputs #69 does not cover.

What is wrong

  • Cells, not code points. wrapRows in src/ui/columns.ts counts code points. Line wraps by rendered cell width, and a CJK character takes two cells. A lane named from a CJK worktree basename therefore measures narrower than it draws, and the six-row cap does not hold.

  • Sanitized after measuring. Home renders the detail through safe() (src/ui/home.tsx), which turns control bytes into spaces. A space is a word break the measurement never saw, so a lane title built from a process environment value can measure as six rows and draw as seven.

  • The floor clamps upward. detailWidth in src/ui/chrome.tsx returns at least 20 columns. A 20-column terminal leaves 16 columns after the screen padding and 14 inside the Detail block, so the measurement claims six columns the card does not have and text capped to six rows draws more. Found in the review of fix(ui): one sentence per launcher conclusion, and a card that fits #69 at head 935f21e. Either measure the real positive width, or say what the card does on a terminal narrower than the 26 columns a 20-column detail needs.

  • The renderer breaks a word where the measurement does not. wrapRows breaks on spaces; the terminal also breaks after a full stop inside a word. Seen in the drawn frame of an open card at 100 columns on 2026-09-12: places it in agents. on one row and slice. on the next, splitting the unit name a reader is being told to use. The measured row count is therefore a lower bound, not the count.

Requirements

  • Measure what the renderer draws: one function answers the width of a string in terminal cells, and the wrap and cut helpers in src/ui/columns.ts use it instead of counting code points. A cut offset is cell-aware too, so a cut never lands inside a wide character.
  • Sanitize every lead and protected sentence before any width decision, so the string measured is the string drawn.
  • Tests: a detail holding a dotted name such as agents.slice draws it unbroken, or the measurement counts the break the renderer makes; a terminal of 20 columns draws no more rows than the budget; a lane name of CJK characters at a narrow width draws no more rows than the budget; a lane title carrying a control byte measures what it draws. Each check fails against the current code.

Context

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions