Median

Pages and highlights

Let a tool offer the customer a page of your app, or point at something on the page.

Updated Oct 1, 20265 minute read
median.config.ts
import { defineConfig, navigation } from "@mediansh/agent-tools";

export default defineConfig({
  plugins: [navigation({ exclude: ["/admin/**", "/internal/**"] })],
  tools: {
    // ...
  },
});
app/support.tsx
"use client";

import { MedianSupport } from "@mediansh/widget";
import { useRouter } from "next/navigation";

export function Support({ publicKey }: { publicKey: string }) {
  const router = useRouter();
  return <MedianSupport apiKey={publicKey} onNavigate={router.push} />;
}

A tool result can carry a page, a highlight, or both. The page becomes a Take me there button under the agent's reply. Nothing moves until the customer presses it. Without onNavigate on the widget there is no button. See Support widget.

WayUse it for
navigation()Any page of a Next.js app, picked by the agent from a list
navigateTo()A page your own tool works out, such as one order
medianHighlightAn element on the page to point at

Offer any page

navigation() is a plugin that adds one tool, goToPage. It reads your Next.js routes when the route module loads and offers every page you do not exclude.

  • The agent picks a page from the list and supplies a value for each bracketed segment. /orders/[id] and 1234 become /orders/1234.
  • Values are URL encoded. A catch-all [...slug] value may contain slashes. An optional [[...slug]] may be left empty.
  • Values are sent comma separated, in bracket order, so one value cannot contain a comma.
  • The page input only accepts pages on the list. The wrong number of values, or an empty value, returns { offered: false, reason } so the agent can try again.
  • goToPage counts toward the 20 tools per organization. It shows on Agent → Tools like any tool and can be switched off.
  • A page on the list returns navigateTo(path, { offered: true, page }). See Offer a page from your own tool.
NameTypeDescription
excludestring[]Pages to leave out, as paths with * for one segment and ** for any number. Default: [].
routesstring[]Your pages, written like /orders/[id]. Skips the scan. Use it for other frameworks and edge runtimes.
dirstringWhere your Next.js app is, if it is not the working directory. Default: process.cwd().
risk"low" | "medium" | "reviewed" | "high"The risk of goToPage. Default: "low".

Exclude pages

PatternMatches
/adminThat page, and nothing under it
/admin/**/admin and everything under it
/orders/*/orders/[id], but not /orders/[id]/refund
/*-internal/billing-internal
/**Every page, which throws

Patterns match pages as Next.js writes them, brackets included, so /orders/[id] excludes that one page. Nothing is excluded by default.

An excluded page is left out of the list the agent gets, so the agent cannot offer it or learn from the list that it exists.

An exclusion that matches no page logs a console warning, Median: navigation() has no pages matching "/admni/**", so it excludes nothing. Check the spelling if you meant to keep a page out.

Where the routes come from

SourceRead when
routesYou pass it. Nothing else is read
app/, src/app/, pages/, src/pages/ under dirThe source is there, as in development or a server deployed from a checkout
.next/server/app-paths-manifest.json and .next/server/pages-manifest.jsonNo source was found, as on Vercel or anywhere else that ships the build output

The scan goes 24 folders deep and reads up to 20,000 files in each folder it starts from. It skips node_modules, .git, .next, dist and build.

What counts as a page

RouterPagesSkipped
Apppage.tsx, page.ts, page.jsx, page.js, page.mdxroute.ts handlers, _private folders, @slot folders, interception routes like (.)photo. Route groups like (marketing) are dropped from the path
Pages.tsx, .ts, .jsx, .js, .mdx files. index is its folder's pathapi/, _app, _document, anything starting with _, 404, 500, and names with an extra dot like button.test.tsx

Load errors

CauseWhat happens
The scan finds no pagesA console error. The plugin adds no tool and the rest of the config works
exclude removes every pageThrows
A pattern that is empty, has a backslash, or does not start with /Throws
routes: []Throws
A route that does not start with /, or has whitespaceThrows
The page list is over 8,000 characters of schemaThrows. Exclude the sections no customer needs
Your own tool is named goToPageThrows

Edge runtimes

The scan reads the disk, which needs Node 20.16 or later, or Bun. On an edge runtime it finds nothing, so pass routes:

navigation({ routes: ["/", "/settings", "/orders/[id]"] })

Offer a page from your own tool

Wrap a result in navigateTo when the page depends on something only your server knows:

median.config.ts
import { defineConfig, navigateTo, p } from "@mediansh/agent-tools";

export default defineConfig({
  tools: {
    openOrder: {
      description: "Offer the customer one of their orders.",
      input: { orderNumber: p.string("The order number.") },
      async execute({ orderNumber }, { visitor }) {
        if (!visitor.verified) return { ok: false, reason: "not_signed_in" };
        const order = await findOrder(visitor.externalId, orderNumber);
        if (order === null) return { found: false };
        return navigateTo(`/orders/${order.id}`, { found: true });
      },
    },
  },
});

navigateTo(path, facts?) returns facts with a medianNavigateTo key. The agent reads facts. Median removes the path before the agent reads the result, so the agent never sees it and cannot invent one.

Path ruleRefused example
Starts with a single /https://acme.com/orders, //acme.com
No backslash/\acme.com
No .. segment, including %2e%2e/orders/../admin
No spaces or control characters/search?q=red shoes
Up to 2,048 characters

Query strings are allowed, as in /orders/1234?tab=refunds.

On a bad path, navigateTo throws This tool cannot navigate there. followed by the reason. The call fails, and the agent is told the tool could not do it. Check a path first with navigationPathIssue(path), which returns the reason or null.

Median checks the path again when the result arrives, and drops one that is not a path on your site.

Point at something

Add medianHighlight, a CSS selector, to a result:

return {
  ...navigateTo(`/orders/${order.id}`, { found: true }),
  medianHighlight: "#refund-button",
};

Without a destination it points at something already on screen:

return { exported: true, medianHighlight: "#export-button" };
RuleDetail
SelectorUp to 256 characters, no control characters. Otherwise dropped
With a destinationDrawn after the customer presses Take me there
Without oneDrawn when the reply arrives. Needs no onNavigate
SearchThe widget looks for the element for 4 seconds, then gives up
RingScrolls the element into view and rings it for about 2.6 seconds. Your page's styles are not changed
Invalid selectorA console error, and nothing is drawn
The agentNever sees the selector

What the customer sees

CaseBehavior
ButtonTake me there, under the reply that explains it. Screen readers hear "Take me to" and the path
No onNavigateNo button. In development the console warns once
PressingThe widget checks the path is on the current site, then calls onNavigate(path)
One per turnA tool that returned a page or highlight cannot run again that turn. If two tools return a page in one turn, the first is kept
Approved or teammate-run callsThe page appears as a button above the message box for 5 minutes, and goes once pressed
Old highlightsA highlight is drawn only if it arrived in the last 5 minutes while the widget was open. Earlier ones are not replayed
ChannelsOnly the widget shows buttons and highlights

Buttons under a reply do not expire.

Still need help?

    Esc