Skip to content

fix: links in chat could not be opened#8544

Merged
matt2e merged 9 commits into
mainfrom
goose-2-project
Apr 21, 2026
Merged

fix: links in chat could not be opened#8544
matt2e merged 9 commits into
mainfrom
goose-2-project

Conversation

@matt2e
Copy link
Copy Markdown
Collaborator

@matt2e matt2e commented Apr 15, 2026

Screenshot 2026-04-15 at 3 51 12 pm

Summary

  • Streamdown showed a dialog asking if the user wanted to open an external link, but that link just tried to navigate the browser to the link, which does nothing in a tauri app
  • We have to intercept the links being clicked in messages and show our own dialog, which opens the link in the user's default browser by calling back into the tauri backend

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f22b0da57

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ui/goose2/src/shared/ui/ai-elements/message.tsx Outdated
Comment thread ui/goose2/src/shared/ui/ai-elements/message.tsx Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b263ba9c7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ui/goose2/src/features/chat/hooks/useArtifactLinkHandler.ts Outdated
@matt2e matt2e changed the title fix: open external links in chat via Tauri opener plugin fix: links in chat could not be opened Apr 15, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4d608ca85

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
className,
)}
linkSafety={linkSafetyConfig}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve local artifact link routing when enabling linkSafety

Enabling linkSafety here changes Streamdown’s link renderer to its intercepted link flow, so markdown links are no longer handled as plain anchors. useArtifactLinkHandler still only matches closest("a"), which means assistant markdown links stop going through resolveMarkdownHref/openResolvedPath; internal artifact paths are then routed through the modal’s openUrl path (or fail), breaking artifact navigation and bypassing the allowed-roots policy.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c02f12a0c3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@@ -0,0 +1,89 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { openUrl } from "@tauri-apps/plugin-opener";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Export openUrl in plugin-opener test mocks

Introducing the openUrl import here means any Vitest mock of @tauri-apps/plugin-opener must now provide that named export; existing mocks like src/features/chat/ui/__tests__/MessageBubble.test.tsx currently define only openPath, so module loading will fail with a missing-export error before assertions run. Please update affected mocks (or use a partial mock of the real module) so chat-related test suites keep running.

Useful? React with 👍 / 👎.

Comment thread ui/goose2/src/shared/ui/ai-elements/link-safety-modal.tsx Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a8cc9f0326

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

"title": "Environment Variables",
"toggleVisibility": "Toggle value visibility"
},
"linkSafety": {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Localize new link-safety keys in Spanish locale

LinkSafetyModal now reads common:components.linkSafety.*, but this commit only adds those keys to locales/en/common.json; locales/es/common.json is still missing them. In Spanish sessions the modal will fall back to English for every new label, creating a visible localization regression on a surface that was previously fully translated.

Useful? React with 👍 / 👎.

Comment on lines +353 to +358
<button
className="wrap-anywhere appearance-none text-left font-medium text-primary underline"
data-streamdown="link"
onClick={() => openModal?.(href ?? "")}
type="button"
>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep external markdown links as anchors for accessibility

Replacing external markdown links with <button> removes link semantics, so assistive tech users can no longer navigate these items via link lists and users lose standard link affordances (e.g., link context actions). This is an accessibility regression versus the previous anchor-based rendering; keep an <a href> element and intercept click behavior to open the safety modal instead of changing the role to a button.

Useful? React with 👍 / 👎.

Comment thread ui/goose2/src/features/chat/lib/artifactPathPolicyCore.ts Outdated
Comment thread ui/goose2/src/features/chat/ui/__tests__/ContextPanel.test.tsx Outdated
@lifeizhou-ap
Copy link
Copy Markdown
Collaborator

lifeizhou-ap commented Apr 17, 2026

Looks good!

I left some comments. Before merging this PR, would you please address these including the i18n one from codex.

Also one thought: would it be possible to keep external markdown links as a href=... and intercept the click to open the modal, instead of rendering them as button?

something like

<a
     href={url}
     onClick={(e) => {
       e.preventDefault();
       openModal(url);
     }}
   >
     ...
   </a>

That would keep the external-link flow the same, while preserving normal link semantics and accessibility. Since the modal and openUrl() handling are already in place, it seems like a pretty small change from the current implementation.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c314e8219f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +7 to +10
vi.mock("@tauri-apps/plugin-opener", () => ({
openPath: vi.fn(),
openUrl: vi.fn(),
}));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Mock revealItemInDir in plugin-opener test setup

The global @tauri-apps/plugin-opener mock now exports only openPath and openUrl, but src/shared/lib/fileManager.ts imports revealItemInDir; any test that exercises revealInFileManager (for example via FileContextMenu) will get an undefined function from this mock and fail at runtime. Add revealItemInDir: vi.fn() to this shared mock so plugin-opener consumers have a complete mocked surface.

Useful? React with 👍 / 👎.

matt2e and others added 8 commits April 21, 2026 15:42
External links in chat messages were silently ignored in Tauri's
WKWebView. Add a custom MarkdownLink component that splits link
behavior by type: external links show a LinkSafetyModal confirmation
dialog before opening via @tauri-apps/plugin-opener, while internal
links render as plain <a> elements so useArtifactLinkHandler can
intercept them for artifact navigation.

This replaces Streamdown's built-in linkSafety which converts all
links to <button> elements, breaking the artifact policy layer that
relies on closest("a") to resolve internal paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move isExternalHref to shared/lib/ to fix dependency inversion where
  shared/ui imported from features/chat
- Reset isCopied state when LinkSafetyModal reopens via useEffect on
  isOpen, preventing stale 'Copied!' text on reopen
- Await openUrl before closing LinkSafetyModal so the modal stays
  visible if the open fails
- Lift modal state to MessageResponse level with a single
  LinkSafetyModal instance and React context, instead of mounting one
  Dialog per external link in the message
- Update useArtifactLinkHandler doc comment to reference MarkdownLink
  instead of the now-disabled Streamdown linkSafety

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
- Update test description from 'Streamdown linkSafety modal' to
  'MarkdownLink'\''s LinkSafetyModal' to match actual implementation
- Clean up isExternalHref import/re-export in artifactPathPolicyCore:
  import once at top for local use, re-export the local binding instead
  of a separate re-export-from-source statement
- Move hardcoded English strings in LinkSafetyModal to react-i18next
  under components.linkSafety namespace for i18n consistency
- Add comment on MessageResponse memo comparator explaining that
  internal state (modalUrl) is intentionally outside the comparator

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
The LinkSafetyModal introduced in c02f12a imports openUrl from
@tauri-apps/plugin-opener. Test files that mock this module only
provided openPath, causing module loading to fail with a missing
export error. Add openUrl: vi.fn() to all four affected mocks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the re-export shim of isExternalHref through artifactPathPolicy
and artifactPathPolicyCore. Consumers now import directly from
@/shared/lib/isExternalHref, eliminating unnecessary indirection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Centralise the @tauri-apps/plugin-opener vi.mock in the goose2 test
setup file so each test suite no longer duplicates it. Tests that
assert on openPath now import it from the mocked module and use
vi.mocked() instead of a local mock variable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change MarkdownLink to render external links as <a> elements with
preventDefault + modal open on click, instead of <button> elements.
This improves accessibility (screen readers, right-click copy, status
bar preview) and makes all chat links semantically consistent.
useArtifactLinkHandler already has an isExternalHref early return so
there is no conflict with the delegated click handler.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The linkSafety i18n keys were added to en/common.json but not
es/common.json, causing Spanish sessions to fall back to English
for the external link confirmation modal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove extra blank lines in artifactPathPolicy.ts and
artifactPathPolicyCore.ts, and wrap long expect call in
FilesList.test.tsx to satisfy biome's formatter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@matt2e matt2e added this pull request to the merge queue Apr 21, 2026
Merged via the queue into main with commit 70e12d9 Apr 21, 2026
23 checks passed
@matt2e matt2e deleted the goose-2-project branch April 21, 2026 06:12
lifeizhou-ap added a commit that referenced this pull request Apr 21, 2026
* main:
  feat(hooks): add Husky git hooks for ui/goose2 (#8577)
  fix: links in chat could not be opened (#8544)
  fix: run setup before dev and dev-debug in goose2 justfile (#8718)
  Manage skills as sources over ACP (#8675)
  handle full node paths in goose2 kill recipe (#8709)
  overhaul provider inventory and agent/model selection (#8652)
  Remove unused import (#8676)
  delete the goose2 migration plan prompt (#8678)
  Add health score badge to README (#8677)
  feat(goose2): voice dictation via direct-ACP pattern (#8609)
  consistently use actions-rust-lang/setup-rust-toolchain (#8671)
  fix(developer): run shell tool under bash/sh regardless of login shell (#8659)
  refactor(providers): extract shared OAuth device-flow helper (#8619)
  Add a goose2 release workflow (#8629)
  chore(deps): bump ncipollo/release-action from 1.20.0 to 1.21.0 (#8664)
  docs: add blog post about Mesh LLM provider option (#8655)
  fix: append /chat/completions for prefixed v1 base URLs (#8521)
  Reset ChatGPT Codex auth during OAuth setup (#8569)
  chore(deps): bump EmbarkStudios/cargo-deny-action from 2.0.15 to 2.0.17 (#8665)
  Add dependabot config for pnpm workspace, cargo, and actions (#8660)
spikewang pushed a commit to spikewang/goose that referenced this pull request Apr 22, 2026
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
lifeizhou-ap added a commit that referenced this pull request Apr 22, 2026
* main: (41 commits)
  removed the specific code owner for documentation change (#8749)
  fix(providers): handle missing delta field in streaming chunks (#8700)
  refactor(providers): extract http_status module and rename handle_status_openai_compat (#8620)
  fix(providers/openai): accept streaming chunks with both reasoning fields (#8715)
  feat: associate threads with projects (#8745)
  upgrade goose sdk and tui to be compatible with the latest agentclientprotocol/sdk package (#8667)
  feat: extend goose2 context window ux with auto-compaction (#8721)
  improve goose2 agent management flows (#8737)
  alexhancock/tui-improvements (#8736)
  fix: add strict:false to Responses API tools and gpt-5.4 to known models (#8636)
  persist and reliably apply chat model selection (#8734)
  merge goose-acp crate into goose (#8726)
  docs: AGENTS.md section on goose2 desktop backend architecture (#8732)
  feat: goose2 message bubble + action tray (#8720)
  consolidate provider ACP methods onto inventory (#8710)
  ci: declare and enforce MSRV of 1.91.1 (#8670)
  fix(ui): correct grammar in apps view description (#8668) (#8679)
  Stop load openai fast model for openapi compatible custom endpoint (#8644)
  feat(hooks): add Husky git hooks for ui/goose2 (#8577)
  fix: links in chat could not be opened (#8544)
  ...
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.

3 participants