Skip to content

fix: enable link click handling in VSelectableTextView (LUM-748)#23986

Merged
ashleeradka merged 1 commit into
mainfrom
devin/1775577360-lum-748-links-not-clickable
Apr 7, 2026
Merged

fix: enable link click handling in VSelectableTextView (LUM-748)#23986
ashleeradka merged 1 commit into
mainfrom
devin/1775577360-lum-748-links-not-clickable

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot commented Apr 7, 2026

Summary

Links in chat messages were visually styled (tint color, underline, pointer cursor) but not clickable. The root cause: VSelectableTextView's NSTextView had linkTextAttributes configured but no delegate to handle click events.

This PR adds NSTextViewDelegate conformance to the existing Coordinator, implements textView(_:clickedOnLink:at:) to open URLs via NSWorkspace.shared.open(), and wires the delegate in makeNSView.

Review & Testing Checklist for Human

  • Verify the project builds in Xcode (CI has no macOS build environment)
  • Test clicking markdown links ([text](url)) in assistant chat messages — should open in default browser
  • Test clicking autolinked bare URLs (e.g. https://example.com) in chat messages
  • Test that text selection (click-drag, Cmd+A, Cmd+C) still works correctly with the delegate attached
  • Verify no regressions in scroll performance with many messages containing links

Notes

  • Coordinator now inherits from NSObject (required for the ObjC delegate protocol). This is standard for NSViewRepresentable coordinators that conform to AppKit delegate protocols.
  • The delegate method handles both URL and String link values, covering all NSAttributedString link attribute types.
  • A previous attempt at this fix existed on an unmerged branch (devin/1774970368-lum-635-selectable-text-view) using the same delegate approach, but it targeted an older file location before VSelectableTextView was moved to the design system.

Link to Devin session: https://app.devin.ai/sessions/c95781541a1246b49843bb46c01d006e
Requested by: @ashleeradka


Open with Devin

Add NSTextViewDelegate to Coordinator with textView(_:clickedOnLink:at:)
to open clicked links in the default browser via NSWorkspace. The NSTextView
was already styling links (tint color, underline, pointer cursor) but had
no delegate to handle click events, making links visually styled but inert.

Closes LUM-748

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +249 to +258
public func textView(_ textView: NSTextView, clickedOnLink link: Any, at charIndex: Int) -> Bool {
if let url = link as? URL {
NSWorkspace.shared.open(url)
return true
}
if let string = link as? String, let url = URL(string: string) {
NSWorkspace.shared.open(url)
return true
}
return false
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.

🚩 Delegate link handler replicates default NSTextView behavior for non-editable text views

For a non-editable NSTextView (isEditable = false), the default behavior already opens links via NSWorkspace.shared.open() when no delegate intercepts the click. The new textView(_:clickedOnLink:at:) at line 249 does exactly what the default handler would do — open the URL in the default browser. This means the explicit delegate is currently a no-op from a behavioral standpoint.

This is likely intentional as a foundation for future customization (e.g., intercepting specific URL schemes, adding analytics, or routing internal links differently). However, if the only goal was to "enable" link clicking, the delegate wasn't needed — links already worked in the non-editable text view. Worth confirming the intent.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

The claim that the default NSTextView behavior already opens links for non-editable text views is not accurate in this configuration. The user confirmed links are visually styled but completely inert (LUM-748 screenshot shows pointer cursor and underline but no click response).

Several factors can prevent the default link-opening fallback from firing in practice:

  1. Explicit TextKit 1 stack — this NSTextView is created with a manually constructed TextKit 1 stack (NSTextStorageNSLayoutManagerNSTextContainer), not the default initializer. The default clicked(onLink:at:) fallback behavior may not be wired identically in this path.
  2. isSelectable = true + isEditable = false — in this mode, mouse events are primarily routed through the selection machinery. Link click detection depends on the text view correctly distinguishing a click-on-link from a selection gesture, which can be unreliable without an explicit delegate.
  3. Previous fix attempt — an earlier branch (devin/1774970368-lum-635-selectable-text-view) independently reached the same conclusion and used the delegate approach.

The explicit delegate is the Apple-recommended pattern for handling link clicks (NSTextViewDelegate.textView(_:clickedOnLink:at:)). It provides reliable, deterministic behavior regardless of the TextKit stack configuration, and gives us a hook for future customization (e.g., routing internal links, analytics).

@ashleeradka ashleeradka merged commit c849c1d into main Apr 7, 2026
5 checks passed
@ashleeradka ashleeradka deleted the devin/1775577360-lum-748-links-not-clickable branch April 7, 2026 16:13
noanflaherty pushed a commit that referenced this pull request Apr 7, 2026
Add NSTextViewDelegate to Coordinator with textView(_:clickedOnLink:at:)
to open clicked links in the default browser via NSWorkspace. The NSTextView
was already styling links (tint color, underline, pointer cursor) but had
no delegate to handle click events, making links visually styled but inert.

Closes LUM-748

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ashlee@vellum.ai <ashlee@vellum.ai>
noanflaherty added a commit that referenced this pull request Apr 7, 2026
* fix: use borderElement for unchecked checkbox visibility (LUM-751) (#23992)

The ToS checkbox border used VColor.borderBase (#24292E dark) which is
identical to VCard's VColor.surfaceLift background (#24292E dark),
making the unchecked checkbox completely invisible in dark mode.

Switch to VColor.borderElement (#5A6672 dark / #CFCCC9 light) which
is the semantic token for interactive element borders and provides
clear contrast against the card background in both color schemes.

Closes LUM-751

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ashlee@vellum.ai <ashlee@vellum.ai>

* fix: enable link click handling in VSelectableTextView (#23986)

Add NSTextViewDelegate to Coordinator with textView(_:clickedOnLink:at:)
to open clicked links in the default browser via NSWorkspace. The NSTextView
was already styling links (tint color, underline, pointer cursor) but had
no delegate to handle click events, making links visually styled but inert.

Closes LUM-748

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ashlee@vellum.ai <ashlee@vellum.ai>

* fix: prevent onboarding from repeating name confirmation after tool calls (#23989)

The agentic loop calls the LLM again after tool results return. Combined
with "talk before you work" (SOUL.md) and "save immediately" (BOOTSTRAP.md),
the model would confirm the user's name, call file_edit, then re-confirm
the name in the continuation response.

Add explicit guidance in both BOOTSTRAP.md and SOUL.md to not repeat text
that was already shown before tool calls.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update Vellum Cloud onboarding copy to emphasize always-on availability (#24022)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [LUM-753] Improve client-side dictation resilience (#24042)

* [LUM-753] Improve dictation resilience: reduce timeout, use warning-level logs, add timing

- Reduce dictation HTTP timeout from 10s to 5s so the client falls back
  to raw transcription faster when the daemon is unreachable
- Downgrade failure logs from error to warning since these are expected
  failures with graceful recovery (not actionable errors)
- Add elapsed time to all failure log messages for debugging latency
- Improve fallback log to include transcription length for diagnostics

Co-Authored-By: tkheyfets <timur@vellum.ai>

* Apply suggestions from code review

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: tkheyfets <timur@vellum.ai>
Co-authored-by: Noa Flaherty <noa@vellum.ai>

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ashlee@vellum.ai <ashlee@vellum.ai>
Co-authored-by: asharma53 <64060709+asharma53@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: tkheyfets <timur@vellum.ai>
vellum-automation Bot added a commit that referenced this pull request Apr 7, 2026
* Release v0.6.2

* Cherry-pick fixes into release/v0.6.2 (#24074)

* fix: use borderElement for unchecked checkbox visibility (LUM-751) (#23992)

The ToS checkbox border used VColor.borderBase (#24292E dark) which is
identical to VCard's VColor.surfaceLift background (#24292E dark),
making the unchecked checkbox completely invisible in dark mode.

Switch to VColor.borderElement (#5A6672 dark / #CFCCC9 light) which
is the semantic token for interactive element borders and provides
clear contrast against the card background in both color schemes.

Closes LUM-751

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ashlee@vellum.ai <ashlee@vellum.ai>

* fix: enable link click handling in VSelectableTextView (#23986)

Add NSTextViewDelegate to Coordinator with textView(_:clickedOnLink:at:)
to open clicked links in the default browser via NSWorkspace. The NSTextView
was already styling links (tint color, underline, pointer cursor) but had
no delegate to handle click events, making links visually styled but inert.

Closes LUM-748

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ashlee@vellum.ai <ashlee@vellum.ai>

* fix: prevent onboarding from repeating name confirmation after tool calls (#23989)

The agentic loop calls the LLM again after tool results return. Combined
with "talk before you work" (SOUL.md) and "save immediately" (BOOTSTRAP.md),
the model would confirm the user's name, call file_edit, then re-confirm
the name in the continuation response.

Add explicit guidance in both BOOTSTRAP.md and SOUL.md to not repeat text
that was already shown before tool calls.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update Vellum Cloud onboarding copy to emphasize always-on availability (#24022)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [LUM-753] Improve client-side dictation resilience (#24042)

* [LUM-753] Improve dictation resilience: reduce timeout, use warning-level logs, add timing

- Reduce dictation HTTP timeout from 10s to 5s so the client falls back
  to raw transcription faster when the daemon is unreachable
- Downgrade failure logs from error to warning since these are expected
  failures with graceful recovery (not actionable errors)
- Add elapsed time to all failure log messages for debugging latency
- Improve fallback log to include transcription length for diagnostics

Co-Authored-By: tkheyfets <timur@vellum.ai>

* Apply suggestions from code review

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: tkheyfets <timur@vellum.ai>
Co-authored-by: Noa Flaherty <noa@vellum.ai>

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ashlee@vellum.ai <ashlee@vellum.ai>
Co-authored-by: asharma53 <64060709+asharma53@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: tkheyfets <timur@vellum.ai>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Noa Flaherty <noa@vellum.ai>
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ashlee@vellum.ai <ashlee@vellum.ai>
Co-authored-by: asharma53 <64060709+asharma53@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: tkheyfets <timur@vellum.ai>
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