SIGN IN SIGN UP

feat(web-search): add parse_link tool to Tavily provider (#721)

* feat(web-search): add parse_link tool to Tavily provider

- Add ParseLinkResult interface and supportsParseLink flag to WebSearch base
- Implement TavilySearch.parseLink using Tavily /extract API
- Inject parse_link based on selected provider's capability via static set
- Show provided tools (Web Search / Read Webpage) in settings UI
- New error code chatbox_parse_link_pro_required for Lite users

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

* feat(web-search): show all tools with supported/unsupported status

List both Web Search and Read Webpage under each provider with check/x
icons, instead of only showing supported ones.

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

* fix(web-search): improve parse_link errors and Pro upgrade link

- Add parse_link_failed (20037) and parse_link_not_supported (20038)
  error codes for third-party provider failures
- Switch chatbox_parse_link_pro_required upgrade CTA from
  OpenSettingButton to OpenMorePlanButton so it actually opens
  the upgrade page instead of root settings

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

* fix(web-search): structured parse_link errors with AI/user dual messages

The Error.message (passed to ChatboxAIAPIError constructor) is what
the AI sees in the tool error result, while detail.i18nKey renders
the localized user-facing message. Previously both were just the
codeName, leaving the AI without a real cause to reason about.

Now technical reasons go to the AI:
- parse_link via Chatbox AI requires Pro license; user is not Pro
- Tavily extract API returned no results for {failedUrl}
- parse_link is not supported by provider \"{provider}\"

User-facing i18n strings stay friendly and actionable.

Also marks ChatboxSearch.supportsParseLink = true so the static
PROVIDERS_WITH_PARSE_LINK set stays consistent with the runtime
class flags (the build-in execute branch still short-circuits
before getParseLinkProvider is consulted).

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

* test(web-search): add parseLinkTool and capability consistency tests

- web-search.test.ts: 12 cases covering parseLinkTool execute paths
  - build-in: Pro check, license check, success, truncate, clamp
  - third-party: routing, error propagation, null result, truncate
- parse-link-consistency.test.ts: 5 cases asserting that
  PROVIDERS_WITH_PARSE_LINK matches each provider's supportsParseLink
  flag, preventing future drift between the static set and class flags

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

* fix(web-search): drop Pro check for parse_link via Chatbox AI

Backend has no Pro restriction on parse_link — any licensed user
(including Lite) can call it. Remove the spurious isPro() gate
and the chatbox_parse_link_pro_required error code (added earlier
in this branch but never released).

The build-in path now only checks for a license key, matching the
backend's actual contract.

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

* docs: document parse_link tool capability per search provider

- Product doc: list both Web Search and Read Webpage tools per
  provider with check/cross matrix; mention setting page indicator
- Technical doc: document supportsParseLink mechanism, the
  PROVIDERS_WITH_PARSE_LINK single source of truth, the consistency
  test, the per-provider execute dispatch, and the AI/user dual
  error message structure

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

* fix(web-search): forward abortSignal in build-in parse_link path

The build-in parse_link branch was not propagating abortSignal,
so cancelling the tool execution still let the underlying network
request continue, while the third-party provider branch already
honored the signal. Adds an optional abortSignal param to
remote.parseUserLinkPro and threads it through afetch's RequestInit.

Reported by CodeRabbit on PR #721.

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

* test(web-search): use path aliases, drop any cast, cover abort path

Address CodeRabbit nitpicks:
- Switch test imports from relative paths to '@/...' aliases
  per project convention
- Replace 'parseLinkTool as any' with a typed ParseLinkToolLike
  interface so signature drift breaks the test
- Add abort signal forwarding tests for both build-in and tavily
  paths to lock in the behaviour fixed in the previous commit
- Document why parse-link-consistency.test.ts maintains its own
  provider list (no central registry yet — follow-up)

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

* fix(web-search): surface ChatboxAI errors in tool call UI

When a tool throws a ChatboxAIAPIError (e.g. tavily_api_key_required,
chatbox_search_license_key_required), the AI SDK turned it into a
tool-error chunk and the UI only showed a generic 'tool call failed'
pill. Users couldn't tell what was wrong or how to fix it.

- Extract ChatboxAIErrorMessage component (from MessageErrTips) so the
  same i18n + action-link rendering can be reused for tool errors
- Preserve errorCode on the tool-call result in stream-chunk-processor
  when the underlying error is a BaseError
- ParseLinkUI / WebSearchGroupUI / GeneralToolCallUI now auto-expand
  on error and render the rich i18n message via ChatboxAIErrorMessage,
  giving users actionable links to settings/upgrade pages
- MessageErrTips refactored to use the shared component

Now selecting Tavily without an API key (or Chatbox AI without a
license) shows the user the actual cause and a one-click path to fix it.

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

* refactor(web-search): extract useAutoExpandOnError, drop narrative comments

/simplify pass on the previous tool error rendering commit:
- Replace 3 copies of useState(isError) + useEffect(() => setExpanded(true))
  with a useAutoExpandOnError hook in ToolCallPartUI
- Remove narrative comments that just restate what the code does
  (auto-expand explanations, "Preserve errorCode for...", cross-file
  pointers in jsdoc) — keep only the why-it-matters note on the hook
- Inline the BaseError check in stream-chunk-processor

No behaviour change.

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

* docs: cover tool error UI surfacing and abortSignal plumbing

- Product doc: explain that failing tools (missing API key, missing
  license) now show actionable in-line guidance with a one-click
  jump to the relevant settings page
- Technical doc: document the tool error rendering pipeline
  (tool execute → stream-chunk-processor → ChatboxAIErrorMessage),
  the abortSignal propagation through remote.parseUserLinkPro,
  and how MessageErrTips and tool call UIs share rendering

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

* fix(request): short-circuit afetch retry on caller-initiated abort

createAfetch and createAuthenticatedAfetch wrapped any fetch failure
in NetworkError and ran the retry loop, so a request the caller had
already aborted (e.g. user cancels a tool execution) silently retried
up to retry+1 times before propagating an obscured error.

Add an isAbortError helper that checks both DOMException('AbortError')
and the request's AbortSignal, and rethrow immediately from both
catch blocks. Skips retries and the 500ms backoff when the user has
already cancelled.

Reported by CodeRabbit on PR #721 (build-in parse_link path).

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

* fix(web-search): tool error UI a11y, fallback, and import aliases

Address CodeRabbit findings on the tool error rendering pass:

- ChatboxAIErrorMessage: switch action Links to component='button'
  type='button' so they are keyboard-focusable and announced as
  buttons by screen readers (the underlying MessageErrTips render
  benefits from the same fix since it now goes through this component)
- ToolCallErrorDetails: only render ChatboxAIErrorMessage when
  ChatboxAIAPIError.getDetail(errorCode) actually exists. For unknown
  BaseError codes (NetworkError, generic ApiError, etc.) the component
  returned null and silently swallowed the underlying error text;
  fall back to the plain Text rendering of errorText
- Switch relative imports of common components to '@/components/common/*'
  per project convention

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
(cherry picked from commit 23d49c19f6bc52a1b53d74c138db6a0d840222d0)
Z
Zeng Xian committed
04d7f934970d532b369112473d76b0e6023ae5c4
Parent: 7b714a2