Skip to content

feat: select a Web-search backend on the Provider form - #412

Draft
MesoX wants to merge 3 commits into
willdady:mainfrom
MesoX:feature/web-backend-catalog-and-selector
Draft

feat: select a Web-search backend on the Provider form#412
MesoX wants to merge 3 commits into
willdady:mainfrom
MesoX:feature/web-backend-catalog-and-selector

Conversation

@MesoX

@MesoX MesoX commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

PR3 — Catalog route, Provider-form selector, canSearch, Sources rendering

Closes the operator-facing half of #329. Third in the stack behind #384 (PR1) and #393 (PR2). Branch feature/web-backend-catalog-and-selector off main @ f48fa24.

Until now a Web-search backend could only be selected by PUTing webBackend on the Provider API, and on a Provider with no native search the Chat Search toggle stayed hidden — so the endpoints this Extension point exists for (vLLM, LiteLLM, Bedrock) could not reach a backend from Chat at all. This makes the selection reachable and presents a backend's results the way native search's are.

20 files, +1373/−80. No migration, no schema change, no change to what a searching turn resolves to.

Opened as a draft on purpose. The code is complete and green, and it has been exercised end-to-end against a real backend (below). It is a draft because of two things I would rather settle before you spend a full review on it: the open questions at the bottom, and the fact that the very control this PR adds is collapsed by the next PR (PR3.5, also below). If you would rather take the collapse first, or want the relabel here instead, this PR changes shape — better to hear that now than after a review pass. Say the word and I mark it ready.

What's in it

GET /organizations/:orgId/web-backends{ results: { backend, name, plugin }[] } from getWebBackends() + getWebBackendPlugin.

Mounted org-scoped, which is a deliberate correction to "mirror sandbox's GET /backends". A sandbox is a workspace resource; a Provider is not — Providers exist in both scopes (ADR-0007) and one ProviderForm serves both from an orgId plus an optional workspaceId. A workspace-scoped catalog is unreachable exactly when an Org Admin edits a Shared Provider, which is the case the selector matters most for. The precedent is GET /plugins, mounted the same way and commented for the same reason: the orgId scopes access, not the data.

No scope filtering is possible or needed: a backend arrives via PLATYPUS_PLUGINS in the backend environment and is registered at boot into a module-level registry. There is no table behind it, so no org or workspace can hold a different list.

Provider form — a nullable select under Advanced settings, beside the Native web search switch, fed by the route through SWR. Shown for every Provider type, including ones with native search, since a set backend wins over native by design.

canSearch — now nativeSearchEnabled !== false && (providerHasNativeSearch(p) || Boolean(p.webBackend)), which is ADR-0014's gate formula verbatim. Gated on the stored id alone: no catalog fetch in chat.tsx, no liveness some() (that is PR9). A stale id degrades server-side to no tools plus a warn line.

Sources rendering — native search surfaces citations as source-url parts. A plugin web_search is client-executed, so without lifting its result the same toggle gives pills on Anthropic and nothing on vLLM. Its results[].url / title now merge into the one <Sources> row, deduplicated by URL.

Four decisions worth your attention

1. The web_search tool card is compact, not the generic renderer. Adding pills alone would have given a vLLM user pills and a collapsible raw-JSON block repeating every result, where Anthropic shows pills alone — which is the outcome ADR-0014's "Presenting results" section rejects. So tool-web_search gets its own branch beside LoadSkillTool: header with the query, body with the answer box and a result count, no JSON. It stays rather than being hidden entirely because the pills cannot say that a search ran, what was searched for, or that it failed — and a failed search returns { error } on a successful part, so there is nothing in the Sources row to notice. read_url keeps the generic card; it is a fetch, not a citation source.

2. The select is not disabled while Native web search is off. The plan's wording was "reads as disabled/inert", but a genuinely disabled select traps a stored backend: clearing it would mean switching native search on, clearing, and switching off again. Concealing a stored value is the same category of trap this section exists to close, so the control stays interactive and the helper text turns into a visible warning instead. Same reasoning behind showing a stored id the catalog no longer lists, labelled (not installed), rather than hiding the field on a deployment with no backends when a value is set.

3. isPresentableUrl moved to @platypus/schemas — which reverses the answer you gave on #384's approval ("stays in web-backends/"). That answer deferred the decision to this PR, on the condition that the frontend becomes the second consumer, which it now is; recording the reversal rather than letting it read as drift, as with PR2's two-warn fold. The frontend needs exactly the scheme filter — length is bounded upstream by MAX_URL_CHARS, with the opposite treatment (drop, not truncate) — so it is lifted verbatim rather than mirrored. Two copies of a security predicate drift, and the frontend's copy is the one deciding what becomes an href. Nothing in apps/frontend imports backend runtime code, so "leave core's alone" would have meant a mirrored predicate, not a shared one. The re-export from web-backends/ is gone and every caller imports from the package; the cases live in packages/schemas/index.test.ts.

4. Telling a plugin web_search from the Provider's own. They share a tool name: services/provider.ts registers native search under web_search on OpenAI, OpenRouter and Anthropic. Matching the name alone — the first shape of this PR — sent a native search to the compact card, which claimed "0 results" for a search that returned ten and hid the vendor payload. One isPluginWebSearchPart predicate now gates both the card and the Sources lifting, layered by how much each check knows:

  1. providerExecuted === true ⇒ native. Definitive where a provider sets it; Anthropic and OpenAI both do, from the first chunk, so there is no flicker.
  2. A core-stamped marker ⇒ plugin. composeWebBackend sets metadata: { platypusWebBackend: true } on both Tools it builds, and the AI SDK carries a Tool's metadata onto the tool call's toolMetadata, onto the UI part, and into the stored message. This is the only check that does not depend on a vendor reporting itself — and it is needed, because @openrouter/ai-sdk-provider never sets providerExecuted anywhere in the package. On providerExecuted alone, a native OpenRouter search would have kept the original bug, in a worse form: no output to count, so a permanent "Searching…".
  3. Core's own result shape, for a finished call with no marker — a message stored before the marker existed. Vendor payloads are shaped differently and do not match.
  4. input-streaming alone falls back to providerExecuted: a marker is attached when the tool call is parsed, and there is no output to recognise yet.

Anything left over reads as native and keeps the generic renderer, which shows whatever it really is. I could not verify OpenRouter empirically — no account — so the fix is written not to need the answer: the marker is true of core's tools on every provider, verified or not. The marker key is exported from @platypus/schemas for decision 3's reason, and pinned by a backend test on the side that writes it.

This is a dependency ADR-0014 does not record. Its Citations consequence says the frontend reads the core-owned results[].url; it must also decide which web_search a part is before reading that shape at all. Queued as a sixth Consequences amendment for PR4, together with the cost it names: the deliberate snake_case exception buys prompt portability by sharing native search's tool name, and the price is that core cannot be identified by name.

The render-side check is not redundant with the backend-side drop. The egress guard covers model-supplied URLs entering read_url; this covers backend-supplied URLs becoming clickable, which nothing else covers, and a backend is third-party code. javascript: and data: results are dropped, and they are dropped before the Sources count too — a "Used 3 sources" row that opens onto one pill is its own bug.

Exercised against a real backend

Core ships no backend by design (ADR-0014 non-goal), which makes this stack hard to try. So I wrote one, out of tree and public, to have something to point a Provider at: https://github.com/MesoX/platypus-searx-backend

It is a plain webBackends contribution — web_search over a self-hosted SearXNG, read_url over obscura via CDP — so no search API key and no third-party account are needed to exercise the Extension point end to end. read_url is contributed only when a browser URL is configured, which also makes the optional-second-tool path a real case rather than a test fixture. The README carries the compose snippets and the two non-default SearXNG settings it needs.

I am not proposing it as a dependency or asking for it to be adopted — it is a reference implementation and a test harness. It happens to be the first third-party consumer of the contract shipped in #384/#393, which is what makes it useful here: everything in this section was found by using the seam, not by reading it.

It works. A self-hosted deployment on a test box, Provider Local (OpenAI + apiMode: "chat", the vLLM shape), backend searx.web: search, citations, paginated reading, the egress block and the stale-selection degrade all behave as specified. The scenario-by-scenario account, with the signal that distinguishes each one and an explicit list of what the manual pass did not cover, is under Verification → Real-world verification below.

Two things came out of running it that a code review will not surface, below.

PR3.5 is a real dependency of this one, not a nice-to-have

This PR ships two controls fighting over one slot — a nativeSearchEnabled switch and a webBackend select, where the switch is a master off for both. That arrangement has no good version, and the state table shows why:

nativeSearchEnabled webBackend Chat globe A searching turn runs
on None shown native vendor search
on set shown the backend; native never reached
off None hidden nothing
off set hidden nothing

Row 4 is the trap: an Operator who switched native search off on a vLLM Provider because native search never worked there selects a backend and finds it silently dead. Flipping row 4 the other way does not fix it — it makes the switch native-only and therefore inert whenever a backend is selected, a control whose effect depends on the value of the control beneath it.

The end state is one three-valued control, searchSource: none | native | <backendId>:

Web search:  ( ) None
             ( ) The Provider's built-in search    ← offered only when providerHasNativeSearch
             ( ) SearXNG (searx.web)

That is PR3.5, promoted out of the optional follow-up list. It carries the migration (search_source + backfill, old columns kept one release), the resolveSearchMode rewrite, the form collapse, and the ADR Consequences edit. ~15 files.

Why it is not folded into this PR: that would put a data migration with a backfill, a change to what a searching turn resolves to, and a new frontend feature carrying a security check (the javascript: pill filter) into one review of ~22–25 files — and you split #384 at a much finer grain than that. The selector here is not throwaway: under PR3.5 it is still a <Select> fed by this route through the same three formData touch points, so most of it survives; what changes is the option list and the bound field. The Switch it replaces already exists on main.

If you would rather have the collapse now and skip the interim shape, say so — that is the main reason this is a draft.

Found while testing: turn-level token growth (not this PR's bug)

Worth recording because the measurement only exists because this stack made searching reachable. On the test box, one searching turn on an Agent reached 229,677 input tokens across 15 steps (7 web_search, 11 read_url) and died on the step ceiling without answering; another reached 67,688 in 9. Every read_url result — 5 000–10 000 chars — stays in the transcript and is re-sent on every subsequent step, so growth is quadratic in reads.

Not web-search-specific and not a regression: a Trigger run with fetchUrl ×2 and updateWidgetData ×9 and no search at all hit 187,066. Core's per-call caps already bound what one call contributes; what is unbounded is what is retained. The portable fix is clearing stale tool results from the transcript (deleting, not summarising — the two compose, and a cleared result is visibly gone where a summary is lossy in a way the model cannot detect). I would file it against the compaction workstream rather than this stack unless you want it here. Happy to open an issue with the numbers if useful.

Open questions

  1. The catalog's read posture. requireAuth + requireOrgAccess(), matching GET /backends, not GET /plugins' admin-only gate. Admin-only would break a real case: a workspace-scoped Provider is editable by a non-admin Workspace Owner when providerSelfManagement is set, and that owner would get an empty dropdown with no explanation. The cost is that any org member can learn which search plugins the deployment has installed, which plugins restricts to admins. The two existing precedents already disagree with each other, so converging them is above this PR — but the choice should be visible rather than inherited silently. Tell me if you want admin-only and I will take the empty-dropdown case instead.
  2. The nativeSearchEnabled relabel is deferred, not dropped. chore(docs): correct the Web-search backend Extension point page #401's out-of-scope note assigns it here. It is deferred because the field disappears in PR3.5, and relabelling a control that is about to be deleted means writing user-facing copy twice and the docs twice on the same surface. The interim mitigation is one sentence at the point of selection plus the visible warning. Say so if you would rather have the relabel now.
  3. PR3.5 before or after this? See above. Default is after.
  4. isPresentableUrl in @platypus/schemas reverses your feat: add the web-search backend Extension point #384 answer, on the condition that answer named (a second consumer). Flagging rather than assuming.
  5. OpenRouter's native search is unverified empirically — no account. The discriminator is written not to depend on it (the marker is core-owned and true on every provider), but if you have a way to check it, scenario 19 is "a native Provider must render with the generic renderer, not the compact card".

One correction to the switch's own description did ship, which my plan had ruled out along with the relabel. Its description read "Turn off for endpoints that don't implement it (e.g. vLLM, Ollama, LiteLLM)" — after PR2 that instructs an Operator to disable the backend on precisely the endpoints this Extension point exists for. That is shipped text walking a user into the trap, not the cosmetic relabel, so I treated the "nothing else" line as drawn one clause too wide and added the exception to the sentence. Still no relabel.

Two related findings are deliberately not fixed here, since they die with the collapse: the providerType !== "Bedrock" condition hiding the switch now hides a control that gates a Bedrock Provider's backend (not a live bug — the column is notNull().default(true), so only a deliberate API write reaches that state), and the switch is still shown on Providers where it is inert.

What did get fixed is the new warning pointing at a control that is not on screen: where the switch is not rendered, the note beside the select names the Provider API as the only way into and out of that state. The condition is untouched; a warning this PR introduces should not create the dead end.

Docs

Three of the four are mandatory because this PR makes shipped text false:

  • extending/index.mdx said "Selection is not on the Provider form yet", and the warning callout under it was built on that premise. Both rewritten; the callout now carries only the coupling that survives.
  • building-with-platypus/chat.mdx listed Bedrock and OpenAI-in-chat-mode as reasons the toggle is absent. Both are now conditional on there being no backend.
  • building-with-platypus/triggers.mdx described the Trigger's Web Search switch as "Model native web search (if supported by provider)". A Trigger's search reaches the same resolveSearchMode, so on a Provider with a backend it searches through it.
  • concepts/providers.mdx gains a new ## Web search section — the page where Provider fields are documented, and there is no building-with-platypus Providers page for CLAUDE.md's table to point at. chore(docs): rewrite the docs for the reader, not the schema #392 stripped every search mention from it, so this is new rather than an amendment. It states the Bedrock case exactly as the form does: the switch is not shown because it is on by default there, and if it was switched off through the API the selection still will not run.

self-hosting/providers-and-auth.mdx is untouched: this PR adds no field limit or env var that page carries. The webBackend 200-char bound and the backend-author caps stay with PR4's extending/web-search-backends.mdx, where a plugin author reads them — a length limit beside a dropdown nobody types into is noise.

Verification

Automated gates

pnpm typecheck and pnpm lint clean workspace-wide. pnpm test: backend 1387, frontend 97, schemas 103, plugin-sdk 7, examples 3 — all passing, re-run on the branch tip against main @ f48fa24 with nothing behind.

apps/docs has one pre-existing failure in docs-contract.test.ts's internal-links case, identical on a clean main (23 pass / 1 fail both ways) — a CRLF artifact of running it on Windows, where the heading regex (^#{1,6}\s+(.*)$) keeps a trailing \r, so every anchor in the tree mis-slugs and the whole check goes empty rather than failing on anything this PR wrote. git config core.autocrlf is true here and the index is LF, so Linux CI is unaffected. Both anchors this PR adds (/concepts/providers#web-search, /extending#web-search-backends) were checked by hand and resolve to real headings. Making the test CRLF-tolerant is a standalone chore, not this stack's.

Real-world verification, against a backend written for this PR

The automated suite mocks the executor, so none of it proves the seam works against third-party code. To have something real to point a Provider at, I wrote a backend out of tree and made it public: https://github.com/MesoX/platypus-searx-backend — a plain webBackends contribution, web_search over self-hosted SearXNG and read_url over obscura via CDP. No search API key and no third-party account, so the whole Extension point is reproducible on any box. It is the first third-party consumer of the contract shipped in #384/#393; everything in the "four decisions" section above was found by using that seam, not by reading it.

Deployment under test: a self-hosted box running the full stack off this branch, Provider Local = OpenAI + apiMode: "chat" (the vLLM shape — a Provider with no native search, i.e. exactly the case this Extension point exists for), backend id searx.web, both services on the compose network. Backend logs tailed on the observability line ({ backend, plugin, tool, durationMs, outcome }) throughout.

Checked by hand, each with a signal that distinguishes it from every other case:

Path What was exercised Signal it actually worked
Search, results globe on, open-ended news query compact card with the query and a count; <Sources> row of ≤10 pills where SearXNG returned 24–26 raw hits, so core's MAX_SEARCH_RESULTS slice is visibly running rather than the backend's raw output
Search, answer, no results a query SearXNG answers from an engine answer box card reports 0 results, the answer renders, and there is no Sources row at all — a zero-result search is not an error
Search, answer + results a query returning both pills and the answer box together, which separates "answer plumbing works" from "answer plumbing works alongside results"
Citation persistence full page reload on a finished turn card and pills survive from the stored message, i.e. the marker and the result shape both round-trip through the DB
read_url, short page a page under the default slice complete content, no continuation hint
read_url, long page a long Wikipedia article at the default max_length 5 000 chars plus [Content truncated. Pass start_index=5000 to continue reading.] and next_start_index: 5000 — byte-identical to fetchUrl, so a model that learned one tool pages the other correctly
Egress guard a model-supplied loopback URL into read_url blocked, the browser is never called, one warn line carrying the policy reason
Stale selection plugin removed from PLATYPUS_PLUGINS, Provider unchanged form shows searx.web (not installed) rather than a blank select; a searching turn injects no tools and warns with providerId
The documented trap Native web search switched off with a backend selected globe absent entirely — the row-4 behaviour PR3.5 removes, confirmed as described rather than assumed

Not covered by the manual pass, stated so it is not read as more than it is. This was a basic-scenarios run, not the full matrix: the failure paths (SearXNG down, obscura down, a slow page hitting outcome: "timeout" rather than error), the boot-refusal paths (a mis-keyed plugin config, an over-ceiling timeoutMs), the hostile-backend stub returning javascript:/data: result URLs, and the over-1M-char content cap were not run against the live deployment — all of them are covered by unit tests instead. Neither was the native-vendor comparison, which needs a second Provider on OpenRouter (no account; see open question 5). The javascript: pill filter in particular is proven by frontend tests, not by a live hostile backend.

New tests

  • Route: plugin annotation, null for an unowned id, empty registry, readable by a non-admin member, 401 unauthenticated, 403 for a non-member, no mutation verb.
  • isPresentableUrl in schemas, including scheme casing.
  • Provider form: absent with no backends installed; options carry the plugin name; None's suffix follows capability; a stored-but-uninstalled id is named; the coupling warning appears only when the switch is off and the select stays enabled; webBackend round-trips through a save and sends null, not "".
  • Chat message: a pill per result with the backend's title; no pill and no href for a javascript: or data: URL; unpresentable results are not counted; URL fallback for a missing title; one pill for a page two searches both returned; merged with native source-url parts; no Sources row on an error; compact card carrying the query instead of the result JSON.
  • Chat message, the native/plugin split: a provider-executed part keeps the generic renderer and lifts no sources, while its source-url citations still render; an unflagged, unmarked part — the OpenRouter shape — does the same; a marked part shows the card mid-call; a result stored before the marker still lifts its pills; a denied call claims no search in flight; a page named by both rows renders one pill, titled by the backend.
  • Backend: composeWebBackend stamps the marker on both Tools — pinned on the side that writes the seam the rendering reads.
  • humanizeToolType: underscores are word boundaries, so web_search, read_url and namespaced MCP names stop rendering as "Web_search".

One rename not called out in its commit message, for completeness: the form's "no backend" sentinel went from __none__ to __platypus_no_web_backend__, since a backend id is an arbitrary string and a contribution registering the old value would have read as None. Never persisted — it is mapped to "" at the control's edge.

frantisek.spacek@morosystems.cz added 3 commits August 3, 2026 16:34
Makes the Web-search backend extension point (ADR-0014) reachable without the
API, and presents a plugin backend's results the way native search's are.

- `GET /organizations/:orgId/web-backends` lists the backends registered at
  boot, each annotated with the plugin that contributed it. Org-scoped rather
  than workspace-scoped, unlike sandbox's `GET /backends`: a Provider can be
  org-scoped (ADR-0007) and one form serves both scopes, so a workspace-scoped
  catalog would be unreachable when an Org Admin edits a Shared Provider.
  Readable by any org member, matching `GET /backends`, because a non-admin
  Workspace Owner may edit a Provider when `providerSelfManagement` is set.

- The Provider form gains a Web-search backend select under Advanced settings,
  shown for every Provider type. It stays interactive while Native web search
  is off and warns instead — disabling it would trap a stored value behind the
  switch. A stored id the catalog no longer lists is shown as "not installed"
  rather than reading as None.

- The Chat search toggle now appears when a Provider has native search *or* a
  configured backend, which is ADR-0014's gate formula. Gated on the stored id
  alone; a stale id degrades server-side.

- A plugin `web_search` is client-executed, so its citations are lifted into
  the same `<Sources>` row native `source-url` parts use, and its tool card is
  compact rather than the generic raw-JSON block (ADR-0014 rejects the blob).
  Every URL is re-checked with `isPresentableUrl` before it becomes an href.

`isPresentableUrl` moves to `@platypus/schemas`, deferred to this PR when the
frontend became its second consumer: two copies of a security predicate drift,
and the frontend's is the one that decides what becomes a link.

Docs: the extending page no longer says selection is API-only, chat.mdx's
toggle-visibility bullets are true again, and `concepts/providers.mdx` gains
the section the field lands on.
Native provider search registers under the same `web_search` tool name as a
Web-search backend's (`services/provider.ts` — OpenAI, OpenRouter, Anthropic),
so the tool name alone cannot identify a plugin result. Discriminating on the
name meant a native search rendered the new compact card: "0 results" for a
search that returned ten, with the vendor payload no longer shown.

One `isPluginWebSearchPart` predicate now gates both the card and the Sources
lifting on `providerExecuted !== true`, and both callers share it rather than
matching `startsWith` in one place and `===` in the other. A native search
keeps the generic renderer and cites through its `source-url` parts.

Alongside, in the same surface:

- The Sources row deduplicates plugin results against the native URLs the same
  message already cites, so a page named by both is one pill and counts once.
- The card says "Searching…" until an output exists, instead of reporting
  0 results for a call still in flight.
- The Provider form distinguishes a catalog that answered from one that failed:
  the field stays when `GET /web-backends` errors and says the list may be
  incomplete, and a stored id is only called "not installed" once the catalog
  has actually been read.
- The backend selector's description states that Native web search gates it,
  and that switch's own description no longer recommends turning it off on the
  endpoints this feature exists for without naming the exception.
- The Trigger form's Web Search switch no longer says "model native web
  search" — a Trigger on a Provider with a backend searches through it.
- `humanizeToolType` splits underscores, so `web_search`, `read_url` and most
  MCP tools stop reading as "Web_search".
- The catalog is sorted by display name in the form; the route returns
  registration order, which follows `PLATYPUS_PLUGINS`.
- `isPresentableUrl` is no longer re-exported from `web-backends/`; callers
  import it from `@platypus/schemas`, where its cases are tested.

Docs: the Bedrock case for the Native web search switch, and the Trigger
step's Web Search wording.
…vider flag

`providerExecuted` alone cannot separate a plugin `web_search` from a
Provider's own. It is each provider package's field to set: Anthropic and
OpenAI set it from the first chunk, but `@openrouter/ai-sdk-provider` never
sets it anywhere, so a native OpenRouter search still reached the compact
card — and after the "Searching…" fix it would sit there permanently rather
than claiming "0 results".

Core owns the Tool it builds, so it marks it. `composeWebBackend` stamps
`metadata: { platypusWebBackend: true }` on both Tools; the AI SDK carries a
Tool's `metadata` onto the tool call's `toolMetadata`, onto the UI part, and
into the stored message, so nothing a provider executes can carry it on any
vendor. The key is exported from `@platypus/schemas` for `isPresentableUrl`'s
reason — the frontend reads what the backend writes — and pinned by a backend
test on the side that writes it.

`isPluginWebSearchPart` is now layered by how much each check knows:
`providerExecuted === true` excludes a native call; the marker identifies
core's; core's own result shape covers a finished call stored before the
marker existed; and only `input-streaming`, where no marker is attached yet
and there is no output to read, falls back to the flag. Anything left over
reads as native and keeps the generic renderer, which shows whatever it
really is.

Alongside, in the same surface:

- The card claims a search is in flight only for the two states where one is.
  A denied call and an `output-error` without `errorText` previously read as
  "Searching…"; the header already reports both.
- Sources deduplication moved to the render site, and the plugin entry now
  wins a collision — it carries the backend's title, where a `source-url`
  part has only the URL. Its comment no longer claims to cover a history
  spanning a Provider change: the dedup is per message, and one turn resolves
  to a backend or to native search, never both.
- Where the Native web search switch is not rendered (Bedrock), the warning
  beside the selector names the Provider API instead of pointing at a control
  that is not on screen. `concepts/providers.mdx` matches, rather than
  promising search runs there unconditionally.
- A test for `humanizeToolType`, including the namespaced MCP names its
  underscore split exists for.
@MesoX

MesoX commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@willdady Hi, I would love to get some input here, before I mark this ready for review. Thanks!

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