Skip to content

feat(app): long links in an agent's questions wrap and open whole - #196

Open
Stener1 wants to merge 3 commits into
cosscom:mainfrom
Stener1:question-card-links
Open

Stener1 wants to merge 3 commits into
cosscom:mainfrom
Stener1:question-card-links

Conversation

@Stener1

@Stener1 Stener1 commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

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.

before

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.ts covers 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) and e2e/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 does e2e/chat.spec.ts: 27/27 with --workers=1. The a11y chat-question scene passes in berth-dark and berth-light.
  • tsc (app and e2e), check:type, check:titles, check:grid, check:csp and pnpm build pass, and so does docs-site build (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

View guided diff

@vercel

vercel Bot commented Oct 27, 2026

Copy link
Copy Markdown

@Stener1 is attempting to deploy a commit to the seanbrydonio's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Stener1

Stener1 commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Opened as a draft only because of the open-PR limit for outside contributors; it's ready for review and will be marked ready when a slot frees. I can't add the run-ci label (no permission on this repo): could a maintainer add it? The checks in AGENTS.md were run locally; results are in the description.

@Stener1
Stener1 marked this pull request as ready for review October 29, 2026 16:10

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 14 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/e2e/ask-card.spec.ts">

<violation number="1" location="app/e2e/ask-card.spec.ts:118">
P3: The `agent.sends` check is a one-time negative assertion that runs right after the `__opened` poll resolves, so an option selection sent a moment later would not fail the test. Wait for the card's interaction to settle before asserting no sends, or assert on a positive signal that the click finished.</violation>
</file>

<file name="app/src/components/conversation/linked-text.tsx">

<violation number="1" location="app/src/components/conversation/linked-text.tsx:23">
P3: This `onKeyDown` stopPropagation is not needed. The click handler already keeps a link inside an option `<label>` from selecting the option. While focus sits on a link, the stop also hides keys from bubble-phase window listeners, such as the Cmd/Ctrl+B sidebar toggle. Remove the line.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread app/src/lib/link-text.ts Resolved
Comment thread app/e2e/ask-card.spec.ts Outdated
await expect(link).toHaveAttribute("href", URL);
await link.click();
await expect.poll(() => app.page.evaluate(() => (window as unknown as { __opened: string[] }).__opened)).toEqual([URL]);
expect(agent.sends).toEqual([]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The agent.sends check is a one-time negative assertion that runs right after the __opened poll resolves, so an option selection sent a moment later would not fail the test. Wait for the card's interaction to settle before asserting no sends, or assert on a positive signal that the click finished.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At app/e2e/ask-card.spec.ts, line 118:

<comment>The `agent.sends` check is a one-time negative assertion that runs right after the `__opened` poll resolves, so an option selection sent a moment later would not fail the test. Wait for the card's interaction to settle before asserting no sends, or assert on a positive signal that the click finished.</comment>

<file context>
@@ -98,6 +98,27 @@ test("a question its screen shows options for still gets its card", async ({ app
+  await expect(link).toHaveAttribute("href", URL);
+  await link.click();
+  await expect.poll(() => app.page.evaluate(() => (window as unknown as { __opened: string[] }).__opened)).toEqual([URL]);
+  expect(agent.sends).toEqual([]);
+  expect(await card.evaluate((el) => el.scrollWidth <= el.clientWidth)).toBe(true);
+});
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 74ce419. The test no longer does a one-time check that nothing was sent. It now clicks Keep mine after the link and polls until agent.sends equals exactly ["2"], so a send the link's click made, even a late one, fails it.

e.stopPropagation();
void openUrl(p.url!);
}}
onKeyDown={(e) => e.stopPropagation()}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This onKeyDown stopPropagation is not needed. The click handler already keeps a link inside an option <label> from selecting the option. While focus sits on a link, the stop also hides keys from bubble-phase window listeners, such as the Cmd/Ctrl+B sidebar toggle. Remove the line.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At app/src/components/conversation/linked-text.tsx, line 23:

<comment>This `onKeyDown` stopPropagation is not needed. The click handler already keeps a link inside an option `<label>` from selecting the option. While focus sits on a link, the stop also hides keys from bubble-phase window listeners, such as the Cmd/Ctrl+B sidebar toggle. Remove the line.</comment>

<file context>
@@ -0,0 +1,31 @@
+          e.stopPropagation();
+          void openUrl(p.url!);
+        }}
+        onKeyDown={(e) => e.stopPropagation()}
+      >
+        {p.text}
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping it. I tried removing it: with focus on a link inside an option, ArrowDown reaches the base-ui radio group and checks an option, and question-links.spec.ts now asserts that doesn't happen. Removing the line fails that test. The Cmd/Ctrl+B listener you mention is SidebarProvider in components/ui/sidebar.tsx, which the app never mounts, and the app's shortcuts listen in the capture phase (use-shortcuts.ts), before this handler runs. A comment on the line now says why it's there.

Comment thread app/src/lib/mock-conversation.ts Resolved
Stener1 and others added 3 commits October 29, 2026 20:21
A question card cut a long address (a sign-in link) off at its edge and
gave no way to open it. Question text, option labels and descriptions,
the review, the in-terminal card and the screen-read "Needs your answer"
card now wrap inside the card, and every http(s) address in them is a
link that opens the whole address. A link inside an option opens without
picking it. An address that runs into the box's 1,000-byte cut ("…") is
shown but not linked. At a phone's width the form's Submit goes under
Back instead of past the card's edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… not at all when cut short

Codex adversarial review: findUrls stops at an apostrophe, so
?q=O'Reill… linked the prefix and slipped past the cut check. Agent text
now keeps apostrophes in an address (findUrls prose mode); the terminal's
matching is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unctuation, and terminal question options link too

From review: "…/a.… or not" linked https://example.com/a. — the address
is trimmed again once the ellipsis is off. The terminal-only question card
links addresses in its options, and the form's status keeps a minimum
width so Submit goes under it at a phone's width, as its comment says.
The ask-card test waits on a later pick instead of a one-time check that
nothing was sent, the link test checks an arrow key on a focused link
picks nothing (why its keydown stops), and the fixtures' sign-in address
is on example.com.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018aGVs1THsu7x3zB6nnPQDJ
@Stener1
Stener1 force-pushed the question-card-links branch from f87ab24 to 74ce419 Compare October 29, 2026 20:30

This branch has not been deployed

No deployments
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