feat(app): long links in an agent's questions wrap and open whole
Summary
When an agent asks a question with a long address in it (a sign-in link, say), the question card cut the address off at its right edge and gave no way to open it. Now the address wraps inside the card, and every http(s) address in the agent's text is a link that opens the whole address.
<QuestionCard>
<Form> # Card overflow-hidden
- <fieldset> # min-inline-size: min-content → as wide as the URL, clipped
+ <fieldset min-w-0>
- {q.question}
+ <LinkedText text={q.question} /> # wrap-anywhere
<label> <Radio/> <OptionText>
- {label} {description}
+ <LinkedText …/> # click/Enter opens, never picks
<Review>
- truncate # cut the question off
+ wrap-anywhere # whole; not linked (inside a <button>)
- footer: Back · status · Submit # Submit ran past the edge at phone width
+ footer flex-wrap, Submit ml-auto
<InTerminal>, and the screen-read "Needs your answer" <Ask>: same LinkedText + wrap-anywhere
linkParts(text) # app/src/lib/link-text.ts, pure
for m in findUrls(text, prose=true) # reuses term-links: trims sentence punctuation, unmatched brackets
skip unless http(s) # mailto:, file:, javascript: stay text
if m runs into the trailing "…" at the end of the text # the box's 1,000-byte cut
skip # shown, never linked: no half addresses
else strip a trailing "…" (the sentence's)
LinkedText → <a href onClick={preventDefault; stopPropagation; openUrl(url)} onKeyDown={stopPropagation}>
findUrls gets a prose flag that keeps an apostrophe inside an address (?q=O'Reilly). Without it, a cut address with an apostrophe linked its prefix. The terminal's matching is unchanged. A new mock scene, ?scene=question-links, has made-up sign-in addresses and one cut short.
Evidence
Before (main + the mock scene, 1440×900): the address runs past the card's edge, and nothing is a link.

After: verify-berthd's new chat-question-links scenario, drive.mjs chat-question-links, OK with 0 console errors. Both hrefs are the literal whole address (231 and 260 chars). A click on the link in "Approved"'s description, then Enter on it, each record window.open(<whole address>) (what openUrl calls outside the Mac app), and "Approved" stays unpicked. A click on the row's label then picks it. At every step the card's scrollWidth ≤ clientWidth.
| 1440 dark | 1440 light |
|---|---|
![]() | ![]() |
| 390 dark | 390 light | cut short: no link | review |
|---|---|---|---|
![]() | ![]() | ![]() | ![]() |
Tests:
src/lib/link-text.test.tscovers a long OAuth URL with a query, trailing punctuation, quotes, several URLs, a URL cut into "…" (with and without an apostrophe), a mid-sentence "…", and non-http schemes.pnpm test: 614/614.e2e/question-links.spec.ts(new, 4 tests) ande2e/ask-card.spec.ts(a new test: an address in the screen-read card links and opens, sending nothing to the agent) pass, and so doese2e/chat.spec.ts: 27/27 with--workers=1. The a11ychat-questionscene passes in berth-dark and berth-light.tsc(app and e2e),check:type,check:titles,check:grid,check:cspandpnpm buildpass, and so doesdocs-sitebuild (changelog). No Go or Rust changes.- Codex review: no findings. Codex adversarial review found one bug (an apostrophe address cut short became a partial link), fixed in the second commit with regression tests.
Not driven: the Mac app's window (Tauri shell.open) and a real box. The mock's browser build goes through window.open, the same openUrl call the app makes. The InTerminal card (a box without the answer capability) has the same LinkedText but no scenario of its own.
Merge Danger
Door: two-way
Front-end rendering and CSS only. Nothing is stored, there's no migration and no flag. Reverting the PR undoes it.
Blast Radius: question cards
The question form, the in-terminal question card and the screen-read "Needs your answer" card. The terminal's link matching keeps its default. At narrow widths the form's footer can now wrap Submit under Back.
🤖 Generated with Claude Code





