Skip to content

fix(web): wrap follow-up composer placeholder instead of clipping - #7144

Open
worflor wants to merge 1 commit into
nexu-io:mainfrom
worflor:fix/composer-follow-up-placeholder-wrap
Open

fix(web): wrap follow-up composer placeholder instead of clipping#7144
worflor wants to merge 1 commit into
nexu-io:mainfrom
worflor:fix/composer-follow-up-placeholder-wrap

Conversation

@worflor

@worflor worflor commented Aug 19, 2026

Copy link
Copy Markdown

Why

The composer's placeholder rotates through the design-toolbox next-step prompts after you've sent a message in a project. Several are full paragraphs (visualPolish is ~270 chars), and the current composer CSS clips them with an ellipsis at every realistic width:

composer width text hidden
780px 75 chars (28%)
640px 99 chars (37%)
456px 127 chars (47%)

Half the suggestion sits behind .

The wrap fix touches four spots because they interlock:

  1. apps/web/src/styles/chat.css had white-space: nowrap + text-overflow: ellipsis on the composer's carousel, inherited from the home-hero context where scenarios are one-liners. Relaxed to pre-wrap + break-word. Using pre-wrap (not normal) preserves the typewriter's trailing spaces at wrap edges so the caret doesn't jitter to the next line for one frame when a space is typed there.
  2. chat.css boosts the composer wrap's min-height to 104px only while the carousel is mounted (:has() selector), so up to four wrapped lines fit inside the composer's own box and the toolbar row underneath stays uncovered. The previous height silently assumed a single-line placeholder because that was the only case the original CSS handled.
  3. chat.css line-clamps the carousel text at four lines as a guardrail. Any future scenario copy above the current ~270-char ceiling still stays inside the wrap's 104px, so a monster prompt cannot overtake the composer chrome.
  4. PlaceholderCarousel rendered the blinking caret as a sibling flex item to the text span. That aligns fine on one line but strands the caret at a cross-axis position once text wraps. Nesting the caret inside the text span puts it in the same inline flow, so it follows the last character across wrapped lines. home-hero.css gives the caret inline-block defaults so the same element works in both the home-hero and composer contexts.

What users will see

After the first message in a project, the composer's rotating placeholder wraps to fit the composer width instead of ellipsis-truncating. The caret sits right after the last typed character on the last wrapped line. Toolbar row underneath stays uncovered.

Home-hero composer is unchanged.

Surface area

  • UI: composer placeholder rendering in apps/web
  • Default behavior change: placeholder wraps where it used to clip; composer wrap grows to 104px while the follow-up carousel is showing

Screenshots

Top plate is main, bottom plate is this branch. Red marks the pre-fix, green marks the fix.

composer-carousel-wrap · 700px

composer-carousel-wrap · 520px

Bug fix verification

Three red specs, one per boundary:

  • apps/web/tests/components/home-hero/PlaceholderCarousel.caret-inline.test.tsx (Vitest, jsdom) locks the DOM shape (caret nested in the text span, not a sibling). Red on main: caret must be a descendant of the text span: expected null not to be null.
  • e2e/ui/composer-carousel-placeholder-wrap.test.ts layout spec (Playwright, Chromium) drives a real fake-agent daemon turn using the existing real-daemon-run.test.ts harness, waits for the visualPolish prompt under prefers-reduced-motion: reduce, measures the carousel at 780/640/456px viewports. Red on main at 456px: carouselHeight: 21, lineCount: 1, whiteSpace: "nowrap", textOverflow: "ellipsis", caretGapDx: -780 (caret 780px left of where the visible text ends).
  • Same file, fuzz spec hammers the CSS with baseline (~270 chars), 2x, 4x, a 600-char no-space unbreakable string, punctuation-only, and CJK text across 780/456/360px composer widths. Asserts the carousel caps at 4 lines and never overflows the wrap. Red on main: line-clamp breach at 456px [double (~540 chars)]: {"lineCount":26,...}.

All three went red on main and green on this branch.

Real-app verification: drove the fix end-to-end through the desktop shell's chat pane (fake-agent runtime, viewport 1440x1100), confirmed the composer's toolbar row stays uncovered, the caret sits after the last visible character on line 4, and the wrap's box math is clean (carouselOverflowsWrap: false, wrapHeight: 104, carouselHeight: 83).

Validation

  • pnpm --dir apps/web exec vitest run -c vitest.config.ts tests/components/home-hero/PlaceholderCarousel.caret-inline.test.tsx tests/components/home-hero/PlaceholderCarousel.paused.test.tsx tests/components/PlaceholderCarousel.test.tsx --maxWorkers=1: 7 passed
  • pnpm --dir e2e exec playwright test -c playwright.config.ts ui/composer-carousel-placeholder-wrap.test.ts --workers=1: 2 passed (~2m)
  • pnpm --filter @open-design/web typecheck: clean
  • pnpm --dir e2e exec tsc -p tsconfig.json --noEmit: clean
  • pnpm guard: one pre-existing failure unrelated to this PR (packaged-leaf boundary complaining that .github/workflows/ci.yml no longer contains its guarded tools-dev + packaged unit + focused E2E block; same failure reproduces on stock main with these edits stashed; no .github/ files touched here).

After a user sends the first message in a project, ChatPane seeds the
composer's PlaceholderCarousel with the design-toolbox next-step prompts.
Several are full paragraphs (visualPolish is ~270 chars) and the composer
CSS clipped them with white-space: nowrap + text-overflow: ellipsis,
hiding 28% of the text at 780px composer width and 47% at 456px.

Fix touches three spots in chat.css and one in PlaceholderCarousel.tsx
because the wrap, the composer's own height, and the caret alignment
all interlock:

1. chat.css relaxed the composer-scoped clip rules to pre-wrap +
   break-word. Using pre-wrap (not normal) preserves the typewriter's
   trailing spaces at wrap edges so the caret doesn't jitter to the
   next line for one frame when a space is typed there.
2. chat.css boosts the composer wrap's min-height to 104px only while
   the carousel is mounted (:has selector), so up to four wrapped
   lines fit inside the composer's own box and the toolbar row
   underneath stays uncovered. The min-height was previously sized
   for a single-line placeholder because that was the only case the
   original CSS handled.
3. chat.css line-clamps the carousel text at four lines as a
   guardrail: any future scenario copy above the current ~270-char
   ceiling still stays inside the wrap's 104px, so a monster prompt
   cannot overtake the composer chrome.
4. PlaceholderCarousel rendered the blinking caret as a sibling flex
   item to the text span. That aligns fine on one line but strands
   the caret at a cross-axis position once text wraps. Nesting the
   caret inside the text span puts it in the same inline flow, so it
   follows the last character across wrapped lines. home-hero.css
   gives the caret inline-block defaults so the same element works
   in both the home-hero and composer contexts.

Home-hero composer is unchanged. Short scenarios still render
single-line with the caret inline right after the last character.

Coverage:
- Vitest structural lock at apps/web/tests/components/home-hero/
  PlaceholderCarousel.caret-inline.test.tsx locks the DOM shape.
- Playwright layout spec at e2e/ui/composer-carousel-placeholder-wrap.test.ts
  drives a real fake-codex daemon turn, waits for visualPolish under
  prefers-reduced-motion, measures the carousel at 780/640/456px.
- Playwright fuzz spec in the same file hammers the CSS with
  baseline (~270 chars), double, quadruple, 600-char unbreakable
  string, punctuation-only, and CJK text across 780/456/360px
  composer widths. All cap at four lines with no wrap overflow.

Both went red on main and green on this branch.
@lefarcen
lefarcen requested a review from mrcfps August 19, 2026 20:07
@lefarcen lefarcen added size/L PR changes 300-700 lines risk/medium Medium risk: regular code changes type/bugfix Bug fix labels Aug 19, 2026
@lefarcen

Copy link
Copy Markdown
Contributor

Thanks @worflor — since this PR is still in draft, I'll hold off the review pass until you mark it ready for review. I've routed it to @mrcfps in the meantime.

@lefarcen

Copy link
Copy Markdown
Contributor

Heads-up: PR #7023 is also open against this area — both PRs touch apps/web/src/styles/chat.css and are aiming to fix the follow-up composer placeholder wrapping instead of clipping. You and @maxmilian might want to compare approaches; sharing this so neither pass gets duplicated.

@lefarcen lefarcen added the needs-design-review Awaiting design review (external UI change); cleared by the Odcrew App applying design-approved label Aug 19, 2026
@worflor
worflor marked this pull request as ready for review August 19, 2026 20:25
@lefarcen lefarcen added the needs-validation Runtime change detected; needs human or /explore agent validation. label Aug 19, 2026
@lefarcen

Copy link
Copy Markdown
Contributor

Thanks for marking this ready, @worflor. Since this changes visible composer behavior, I've added the QA-required label so it gets a manual pass before merge — please hold off self-merging for now; we'll loop QA in once it's merge-ready.

@mrcfps mrcfps 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.

@worflor thanks for the careful wrap fix — this is a really solid bug follow-up.

I reviewed the five-file diff against the stated goal (follow-up composer placeholders wrap instead of ellipsis-clipping, caret stays on the last glyph, toolbar stays uncovered, home-hero stays single-line).

Verified:

  • PlaceholderCarousel nests the caret in the text span so it can follow wrapped glyphs, while home-hero CSS keeps nowrap + ellipsis so the landing composer does not pick up the new wrap behavior.
  • Composer-scoped pre-wrap / break-word / 4-line clamp plus the :has() 104px min-height are internally consistent with the 13px × 1.6 × 4 + padding math, and they do not leak into .home-hero__prompt-editor.
  • Tests sit on the right seams: the jsdom spec locks the DOM shape, and the Playwright file covers the real follow-up path plus a CSS fixture matrix (baseline / 2× / 4× / unbreakable / punctuation / CJK across widths).

No correctness, safety, or maintainability issues in the changed ranges. Nice work on the red-on-main / green-on-branch loop.

🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.

@lefarcen

Copy link
Copy Markdown
Contributor

Thanks @worflor — closing the loop on my earlier note: @mrcfps has now approved the current head, and I don't have anything additional to add beyond that review.

The remaining sign-off/validation labels on the PR are still outstanding, so once those clear this should be in good shape.

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

Labels

needs-design-review Awaiting design review (external UI change); cleared by the Odcrew App applying design-approved needs-validation Runtime change detected; needs human or /explore agent validation. risk/medium Medium risk: regular code changes size/L PR changes 300-700 lines type/bugfix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants