add navigateToPage assistant tool (#4299)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Zeno Kapitein
2026-06-12 11:57:55 +02:00
committed by GitHub
parent c727cc8b9d
commit 2ecbdd8f62
47 changed files with 426 additions and 4 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Add a `navigateToPage` assistant tool that lets the AI open a documentation page on the reader's behalf. The page opens instantly (no confirmation) and is pushed to the browser history so the reader can navigate back.
@@ -0,0 +1,89 @@
import { describe, expect, it } from 'bun:test';
import { normalizePathname, resolveNavigationTarget, toInSiteHref } from './navigation';
const location = {
href: 'https://docs.example.com/guides/intro',
origin: 'https://docs.example.com',
};
describe('resolveNavigationTarget', () => {
it('resolves an absolute same-origin URL to a relative href and pathname', () => {
expect(
resolveNavigationTarget('https://docs.example.com/reference/models', location)
).toEqual({ href: '/reference/models', pathname: '/reference/models' });
});
it('keeps the query string and section anchor in href but not in pathname', () => {
expect(
resolveNavigationTarget(
'https://docs.example.com/reference/models?tab=api#usage',
location
)
).toEqual({ href: '/reference/models?tab=api#usage', pathname: '/reference/models' });
});
it('resolves a relative path against the current location', () => {
expect(resolveNavigationTarget('/reference/models', location)).toEqual({
href: '/reference/models',
pathname: '/reference/models',
});
});
it('rejects a URL pointing to an external site', () => {
const result = resolveNavigationTarget('https://evil.example.org/phishing', location);
expect('error' in result).toBe(true);
});
});
describe('toInSiteHref', () => {
// A docs site served under `/docs/` on a host shared with other content/sites.
const linker = {
siteBasePath: '/docs/',
toLinkForContent: (url: string) => {
const parsed = new URL(url);
// Mirrors the real linker: in-site iff same host AND under the site base path.
if (parsed.hostname === 'gitbook.com' && parsed.pathname.startsWith('/docs/')) {
return parsed.pathname + parsed.search + parsed.hash;
}
return url;
},
};
it('accepts an in-site absolute URL and returns a relative path', () => {
expect(toInSiteHref('https://gitbook.com/docs/guides/intro', linker)).toBe(
'/docs/guides/intro'
);
});
it('accepts an in-site relative path under the site base path', () => {
expect(toInSiteHref('/docs/guides/intro?x=1#y', linker)).toBe('/docs/guides/intro?x=1#y');
});
it('rejects another page on the same host but outside the site base path', () => {
// The reviewer's case: same host, different site/section.
expect(toInSiteHref('https://gitbook.com/pricing', linker)).toBeNull();
expect(toInSiteHref('/pricing', linker)).toBeNull();
});
it('rejects an external host', () => {
expect(toInSiteHref('https://evil.example.org/docs/guides', linker)).toBeNull();
});
});
describe('normalizePathname', () => {
it('strips a trailing slash', () => {
expect(normalizePathname('/guides/intro/')).toBe('/guides/intro');
});
it('keeps the root slash', () => {
expect(normalizePathname('/')).toBe('/');
});
it('decodes percent-encoding so encoded and decoded paths compare equal', () => {
expect(normalizePathname('/h%C3%A9llo')).toBe(normalizePathname('/héllo'));
});
it('treats encoded and decoded paths with a trailing slash as equal', () => {
expect(normalizePathname('/caf%C3%A9/')).toBe(normalizePathname('/café'));
});
});
@@ -0,0 +1,76 @@
import { withTrailingSlash } from '@/lib/paths';
import { isExternalLink } from '../utils/link';
/**
* Resolve a link into a same-site target to navigate to.
*
* Returns an `error` when the URL is malformed or points outside of the documentation site,
* so the assistant can be told it could not navigate.
*/
export function resolveNavigationTarget(
url: string,
location: { href: string; origin: string }
): { href: string; pathname: string } | { error: string } {
let target: URL;
try {
target = new URL(url, location.href);
} catch {
return { error: `Invalid URL: ${url}` };
}
// Only allow navigating within the current documentation site to avoid sending the user to
// an external website without their consent.
if (isExternalLink(target.href, location.origin)) {
return { error: 'Cannot navigate to a page outside of this documentation site.' };
}
return { href: `${target.pathname}${target.search}${target.hash}`, pathname: target.pathname };
}
/**
* Return a site-relative href if `url` points within the current site, otherwise `null`.
*
* Unlike a bare same-origin check, this enforces the site base path, so an assistant cannot
* navigate the reader to another page on the same host (e.g. a marketing page, or a different
* docs site sharing the host such as `gitbook.com/other` or another `/url/...` proxied site).
*/
export function toInSiteHref(
url: string,
linker: { toLinkForContent: (url: string) => string; siteBasePath: string }
): string | null {
if (URL.canParse(url)) {
// toLinkForContent returns a site-relative path for in-site URLs (matching host AND site
// base path), or the raw absolute URL otherwise.
const link = linker.toLinkForContent(url);
return URL.canParse(link) ? null : link;
}
// Relative path: it must live under the site base path.
let pathname: string;
let rest = '';
try {
const parsed = new URL(url, 'https://navigation.invalid');
pathname = parsed.pathname;
rest = `${parsed.search}${parsed.hash}`;
} catch {
return null;
}
return withTrailingSlash(pathname).startsWith(linker.siteBasePath)
? `${pathname}${rest}`
: null;
}
/**
* Normalize a pathname so two equivalent paths compare equal regardless of percent-encoding or a
* trailing slash (e.g. `/h%C3%A9llo/` and `/héllo`). Used to detect when an SPA navigation has
* committed by comparing against `window.location.pathname`.
*/
export function normalizePathname(pathname: string): string {
let decoded = pathname;
try {
decoded = decodeURIComponent(pathname);
} catch {
// Keep the raw value if it isn't valid percent-encoding.
}
return decoded.length > 1 && decoded.endsWith('/') ? decoded.slice(0, -1) : decoded;
}
@@ -1,2 +1,3 @@
export * from './types';
export * from './chat';
export * from './navigate';
@@ -0,0 +1,48 @@
'use server';
import { resolveContentRef, resolveStringContentRef } from '@/lib/references';
import { fetchServerActionSiteContext, getServerActionBaseContext } from '@/lib/server-actions';
import { traceErrorOnly } from '@/lib/tracing';
import { toInSiteHref } from '../navigation';
/**
* Resolve a link provided by the assistant into a path that can be navigated to within the site.
*
* The assistant references pages using the stable content-ref scheme (e.g.
* `/spaces/<spaceId>/pages/<pageId>`). Those URLs are not directly navigable in the published
* site, so we resolve them to the real site link using the site context. Any other URL is only
* accepted if it points within the current site, so the assistant cannot navigate the reader off
* the documentation site.
*/
export async function resolveAINavigationLink(
url: string
): Promise<{ href: string } | { error: string }> {
return traceErrorOnly('AI.resolveAINavigationLink', async () => {
const baseContext = await getServerActionBaseContext();
const context = await fetchServerActionSiteContext(baseContext);
// The content-ref scheme operates on the path portion of the URL. Strip any origin so an
// absolute URL (e.g. `https://docs.example.com/spaces/.../pages/...`) is handled too.
let path = url;
if (URL.canParse(url)) {
const parsed = new URL(url);
path = `${parsed.pathname}${parsed.search}${parsed.hash}`;
}
const contentRef = resolveStringContentRef(path);
if (contentRef) {
const resolved = await resolveContentRef(contentRef, context);
if (!resolved) {
return { error: `Could not resolve page for ${url}` };
}
return { href: resolved.href };
}
// Not a content reference: only navigate to it if it points within the current site.
const inSiteHref = toInSiteHref(url, context.linker);
if (!inSiteHref) {
return { error: 'Cannot navigate to a page outside of this documentation site.' };
}
return { href: inSiteHref };
});
}
+4 -2
View File
@@ -2,7 +2,9 @@ import type { GitBookIntegrationTool } from '@gitbook/browser-types';
import { integrationsAssistantTools } from '../Integrations';
import { type AnyAIControlTool, getControlTools } from './controls';
export function getTools(): (GitBookIntegrationTool | AnyAIControlTool)[] {
export function getTools(
builtInTools: GitBookIntegrationTool[] = []
): (GitBookIntegrationTool | AnyAIControlTool)[] {
const integrationTools = integrationsAssistantTools.getState().tools;
return [...getControlTools(), ...integrationTools];
return [...getControlTools(), ...builtInTools, ...integrationTools];
}
@@ -23,6 +23,7 @@ import { type AIChatReference, serializeReferences } from './references';
import { type RenderAIMessageOptions, streamAIChatResponse } from './server-actions';
import { getTools } from './tools';
import { useAIMessageContextRef } from './useAIMessageContext';
import { useNavigateToPageTool } from './useNavigateToPageTool';
export type AIChatMessage = {
role: AIMessageRole;
@@ -192,6 +193,10 @@ export function AIChatProvider(props: {
const { siteSpaceId } = useCurrentContent();
const language = useLanguage();
// Built-in tools exposed to the assistant (e.g. navigating to a page). The tool has a stable
// identity, so it can be referenced directly from the streaming callback.
const navigateToPageTool = useNavigateToPageTool();
// Event listeners storage
const eventsRef = React.useRef<Map<AIChatEvent['type'], AIChatEventListener[]>>(new Map());
@@ -256,7 +261,7 @@ export function AIChatProvider(props: {
// Execute a tool call
const executeToolCall = async (event: AIStreamResponseToolCallPending) => {
const tools = getTools();
const tools = getTools([navigateToPageTool]);
const toolDef = tools.find((tool) => tool.name === event.toolCall.tool);
if (!toolDef || !('execute' in toolDef)) {
@@ -292,7 +297,7 @@ export function AIChatProvider(props: {
let toolToExecute: AIStreamResponseToolCallPending | null = null;
try {
const tools = getTools();
const tools = getTools([navigateToPageTool]);
const stream = await streamAIChatResponse({
message: input.message,
toolCall: input.toolCall,
@@ -473,6 +478,7 @@ export function AIChatProvider(props: {
renderMessageOptions?.withToolCalls,
renderMessageOptions?.asEmbeddable,
language,
navigateToPageTool,
]
);
@@ -0,0 +1,117 @@
'use client';
import { useLanguage } from '@/intl/client';
import { tString } from '@/intl/translate';
import type { AIToolDefinition } from '@gitbook/api';
import type { GitBookIntegrationTool } from '@gitbook/browser-types';
import { useRouter } from 'next/navigation';
import * as React from 'react';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { NavigationStatusContext } from '../hooks';
import { normalizePathname, resolveNavigationTarget } from './navigation';
import { resolveAINavigationLink } from './server-actions';
const NavigateToPageInputSchema = z.object({
url: z
.string()
.describe(
'The URL of the documentation page to open. Must be a page within this documentation site (the same URL you would use to link to the page). Can include a section anchor (e.g. #section).'
),
});
/**
* Resolve once the SPA navigation to `pathname` has committed (the browser URL reflects it), or
* after a timeout. App Router updates `window.location` only when the navigation commits, so this
* lets the tool hold its turn until the user is actually on the new page — after which the
* tool-result server action's router refresh can no longer cancel the navigation.
*/
function waitForNavigationCommit(pathname: string): Promise<boolean> {
const target = normalizePathname(pathname);
if (normalizePathname(window.location.pathname) === target) {
return Promise.resolve(true);
}
return new Promise((resolve) => {
const startedAt = Date.now();
const check = () => {
if (normalizePathname(window.location.pathname) === target) {
resolve(true);
} else if (Date.now() - startedAt > 3000) {
resolve(false);
} else {
requestAnimationFrame(check);
}
};
requestAnimationFrame(check);
});
}
/**
* Build the built-in `navigateToPage` tool exposed to the assistant.
*
* The tool opens a page within the current documentation site without confirmation. It navigates
* instantly with the Next.js router (adding a browser history entry, so the user can navigate
* back) and waits for the navigation to commit before reporting back, so the assistant's
* follow-up does not cancel the navigation.
*/
export function useNavigateToPageTool(): GitBookIntegrationTool {
const router = useRouter();
const language = useLanguage();
const { onNavigationClick } = React.useContext(NavigationStatusContext);
// The tool object is memoized once, so read the latest values from a ref at call time.
const ref = React.useRef({ router, language, onNavigationClick });
React.useEffect(() => {
ref.current = { router, language, onNavigationClick };
});
return React.useMemo<GitBookIntegrationTool>(
() => ({
name: 'navigateToPage',
description:
'Navigate the user to a page in the documentation. The page opens instantly without asking for confirmation, so only use it when the user clearly wants to be taken to a specific page. Provide the URL of the page within this documentation site.',
inputSchema: zodToJsonSchema(
NavigateToPageInputSchema as any
) as AIToolDefinition['inputSchema'],
execute: async (input) => {
const { router, language, onNavigationClick } = ref.current;
const { url } = NavigateToPageInputSchema.parse(input);
// The assistant references pages using the stable content-ref scheme
// (e.g. `/spaces/<id>/pages/<id>`). Resolve it server-side to the real site link.
const resolved = await resolveAINavigationLink(url);
const target =
'error' in resolved
? resolved
: resolveNavigationTarget(resolved.href, window.location);
if ('error' in target) {
return {
output: { error: target.error },
summary: {
icon: 'triangle-exclamation',
text: tString(language, 'ai_chat_tools_navigate_failed'),
},
};
}
onNavigationClick(target.href);
router.push(target.href);
const committed = await waitForNavigationCommit(target.pathname);
if (!committed) {
// biome-ignore lint/suspicious/noConsole: surfaces a navigation that never committed
console.warn(`navigateToPage: navigation to ${target.href} did not commit`);
}
return {
output: { navigated: true, url: target.href },
summary: {
icon: 'book-open',
text: tString(language, 'ai_chat_tools_navigated_to_page'),
},
};
},
}),
[]
);
}
@@ -135,6 +135,8 @@ export const ar: TranslationLanguage = {
searched_for: 'تم البحث عن ${1}',
ai_chat_tools_listed_pages: 'تم تصفح الوثائق',
ai_chat_tools_read_page: 'تمت قراءة الصفحة ${1}',
ai_chat_tools_navigated_to_page: 'تم فتح الصفحة',
ai_chat_tools_navigate_failed: 'تعذّر فتح الصفحة',
ai_chat_tools_mcp_tool: 'تم استدعاء ${1}',
ai_chat_ask: 'اسأل ${1}',
ai_chat_ask_about_page: 'اسأل ${1} عن هذه الصفحة',
@@ -137,6 +137,8 @@ export const bg: TranslationLanguage = {
searched_for: 'Търсено ${1}',
ai_chat_tools_listed_pages: 'Прегледа документацията',
ai_chat_tools_read_page: 'Прочете страница ${1}',
ai_chat_tools_navigated_to_page: 'Страницата е отворена',
ai_chat_tools_navigate_failed: 'Страницата не може да бъде отворена',
ai_chat_tools_mcp_tool: 'Извика ${1}',
ai_chat_ask: 'Попитайте ${1}',
ai_chat_ask_about_page: 'Попитайте ${1} за тази страница',
@@ -137,6 +137,8 @@ export const cs: TranslationLanguage = {
searched_for: 'Hledáno ${1}',
ai_chat_tools_listed_pages: 'Procházel dokumentaci',
ai_chat_tools_read_page: 'Přečetl stránku ${1}',
ai_chat_tools_navigated_to_page: 'Stránka otevřena',
ai_chat_tools_navigate_failed: 'Stránku se nepodařilo otevřít',
ai_chat_tools_mcp_tool: 'Zavolal ${1}',
ai_chat_ask: 'Zeptat se ${1}',
ai_chat_ask_about_page: 'Zeptat se ${1} na tuto stránku',
@@ -136,6 +136,8 @@ export const da: TranslationLanguage = {
searched_for: 'Søgte efter ${1}',
ai_chat_tools_listed_pages: 'Gennemså dokumentationen',
ai_chat_tools_read_page: 'Læste side ${1}',
ai_chat_tools_navigated_to_page: 'Åbnede siden',
ai_chat_tools_navigate_failed: 'Kunne ikke åbne siden',
ai_chat_tools_mcp_tool: 'Kaldte ${1}',
ai_chat_ask: 'Spørg ${1}',
ai_chat_ask_about_page: 'Spørg ${1} om denne side',
@@ -139,6 +139,8 @@ export const de = {
searched_for: 'Gesucht nach ${1}',
ai_chat_tools_listed_pages: 'Docs durchsucht',
ai_chat_tools_read_page: 'Seite ${1} gelesen',
ai_chat_tools_navigated_to_page: 'Seite geöffnet',
ai_chat_tools_navigate_failed: 'Seite konnte nicht geöffnet werden',
ai_chat_tools_mcp_tool: '${1} aufgerufen',
ai_chat_ask: '${1} fragen',
ai_chat_ask_about_page: '${1} zu dieser Seite befragen',
@@ -139,6 +139,8 @@ export const el: TranslationLanguage = {
searched_for: 'Αναζήτηση για ${1}',
ai_chat_tools_listed_pages: 'Περιηγήθηκε στην τεκμηρίωση',
ai_chat_tools_read_page: 'Διαβάστηκε η σελίδα ${1}',
ai_chat_tools_navigated_to_page: 'Άνοιξε η σελίδα',
ai_chat_tools_navigate_failed: 'Αποτυχία ανοίγματος της σελίδας',
ai_chat_tools_mcp_tool: 'Κλήθηκε ${1}',
ai_chat_ask: 'Ρωτήστε ${1}',
ai_chat_ask_about_page: 'Ρωτήστε ${1} για αυτήν τη σελίδα',
@@ -133,6 +133,8 @@ export const en = {
searched_for: 'Searched for ${1}',
ai_chat_tools_listed_pages: 'Browsed the docs',
ai_chat_tools_read_page: 'Read page ${1}',
ai_chat_tools_navigated_to_page: 'Opened the page',
ai_chat_tools_navigate_failed: 'Failed to open the page',
ai_chat_tools_mcp_tool: 'Called ${1}',
ai_chat_ask: 'Ask ${1}',
ai_chat_ask_about_page: 'Ask ${1} about this page',
@@ -139,6 +139,8 @@ export const es: TranslationLanguage = {
searched_for: 'Se buscó: ${1}',
ai_chat_tools_listed_pages: 'Exploró los docs',
ai_chat_tools_read_page: 'Leyó la página ${1}',
ai_chat_tools_navigated_to_page: 'Página abierta',
ai_chat_tools_navigate_failed: 'No se pudo abrir la página',
ai_chat_tools_mcp_tool: 'Llamó a ${1}',
ai_chat_ask: 'Preguntar a ${1}',
ai_chat_ask_about_page: 'Preguntar a ${1} sobre esta página',
@@ -135,6 +135,8 @@ export const et: TranslationLanguage = {
searched_for: 'Otsiti ${1}',
ai_chat_tools_listed_pages: 'Sirvis dokumentatsiooni',
ai_chat_tools_read_page: 'Luges lehte ${1}',
ai_chat_tools_navigated_to_page: 'Leht avatud',
ai_chat_tools_navigate_failed: 'Lehe avamine ebaõnnestus',
ai_chat_tools_mcp_tool: 'Kutsus ${1}',
ai_chat_ask: 'Küsi ${1}',
ai_chat_ask_about_page: 'Küsi ${1} selle lehe kohta',
@@ -136,6 +136,8 @@ export const fi: TranslationLanguage = {
searched_for: 'Haettu: ${1}',
ai_chat_tools_listed_pages: 'Selattiin dokumentaatiota',
ai_chat_tools_read_page: 'Luettiin sivu ${1}',
ai_chat_tools_navigated_to_page: 'Sivu avattu',
ai_chat_tools_navigate_failed: 'Sivun avaaminen epäonnistui',
ai_chat_tools_mcp_tool: 'Kutsuttiin ${1}',
ai_chat_ask: 'Kysy ${1}',
ai_chat_ask_about_page: 'Kysy ${1} tältä sivulta',
@@ -134,6 +134,8 @@ export const fr = {
searched_for: 'Recherche: ${1}',
ai_chat_tools_listed_pages: 'A parcouru la documentation',
ai_chat_tools_read_page: 'A consulté la page ${1}',
ai_chat_tools_navigated_to_page: 'Page ouverte',
ai_chat_tools_navigate_failed: "Échec de l'ouverture de la page",
ai_chat_tools_mcp_tool: 'A appelé ${1}',
ai_chat_ask: 'Demander à ${1}',
ai_chat_ask_about_page: 'Demander à ${1} à propos de cette page',
@@ -134,6 +134,8 @@ export const he: TranslationLanguage = {
searched_for: 'חיפש ${1}',
ai_chat_tools_listed_pages: 'עיין בתיעוד',
ai_chat_tools_read_page: 'קרא את הדף ${1}',
ai_chat_tools_navigated_to_page: 'הדף נפתח',
ai_chat_tools_navigate_failed: 'פתיחת הדף נכשלה',
ai_chat_tools_mcp_tool: 'קרא ל-${1}',
ai_chat_ask: 'שאל את ${1}',
ai_chat_ask_about_page: 'שאל את ${1} על הדף הזה',
@@ -135,6 +135,8 @@ export const hi: TranslationLanguage = {
searched_for: '${1} के लिए खोजा गया',
ai_chat_tools_listed_pages: 'दस्तावेज़ ब्राउज़ किए',
ai_chat_tools_read_page: 'पृष्ठ ${1} पढ़ा',
ai_chat_tools_navigated_to_page: 'पेज खोला गया',
ai_chat_tools_navigate_failed: 'पेज खोलने में विफल',
ai_chat_tools_mcp_tool: '${1} को कॉल किया',
ai_chat_ask: '${1} से पूछें',
ai_chat_ask_about_page: 'इस पृष्ठ के बारे में ${1} से पूछें',
@@ -137,6 +137,8 @@ export const hr: TranslationLanguage = {
searched_for: 'Pretraženo ${1}',
ai_chat_tools_listed_pages: 'Pregledao dokumentaciju',
ai_chat_tools_read_page: 'Pročitao stranicu ${1}',
ai_chat_tools_navigated_to_page: 'Stranica otvorena',
ai_chat_tools_navigate_failed: 'Stranicu nije moguće otvoriti',
ai_chat_tools_mcp_tool: 'Pozvao ${1}',
ai_chat_ask: 'Pitaj ${1}',
ai_chat_ask_about_page: 'Pitaj ${1} o ovoj stranici',
@@ -137,6 +137,8 @@ export const hu: TranslationLanguage = {
searched_for: 'Keresve: ${1}',
ai_chat_tools_listed_pages: 'Dokumentáció böngészve',
ai_chat_tools_read_page: '${1} oldal elolvasva',
ai_chat_tools_navigated_to_page: 'Oldal megnyitva',
ai_chat_tools_navigate_failed: 'Az oldal megnyitása sikertelen',
ai_chat_tools_mcp_tool: '${1} meghívva',
ai_chat_ask: '${1} kérdezése',
ai_chat_ask_about_page: '${1} kérdezése erről az oldalról',
@@ -136,6 +136,8 @@ export const id: TranslationLanguage = {
searched_for: 'Mencari ${1}',
ai_chat_tools_listed_pages: 'Menelusuri dokumentasi',
ai_chat_tools_read_page: 'Membaca halaman ${1}',
ai_chat_tools_navigated_to_page: 'Halaman dibuka',
ai_chat_tools_navigate_failed: 'Gagal membuka halaman',
ai_chat_tools_mcp_tool: 'Memanggil ${1}',
ai_chat_ask: 'Tanya ${1}',
ai_chat_ask_about_page: 'Tanya ${1} tentang halaman ini',
@@ -138,6 +138,8 @@ export const it: TranslationLanguage = {
searched_for: 'Ricerca: ${1}',
ai_chat_tools_listed_pages: 'Ha esplorato la documentazione',
ai_chat_tools_read_page: 'Ha letto la pagina ${1}',
ai_chat_tools_navigated_to_page: 'Pagina aperta',
ai_chat_tools_navigate_failed: 'Impossibile aprire la pagina',
ai_chat_tools_mcp_tool: 'Ha chiamato ${1}',
ai_chat_ask: 'Chiedi a ${1}',
ai_chat_ask_about_page: 'Chiedi a ${1} riguardo a questa pagina',
@@ -137,6 +137,8 @@ export const ja: TranslationLanguage = {
searched_for: '${1}を検索しました',
ai_chat_tools_listed_pages: 'ドキュメントを閲覧',
ai_chat_tools_read_page: 'ページ ${1} を読みました',
ai_chat_tools_navigated_to_page: 'ページを開きました',
ai_chat_tools_navigate_failed: 'ページを開けませんでした',
ai_chat_tools_mcp_tool: '${1} を呼び出しました',
ai_chat_ask: '${1} に質問する',
ai_chat_ask_about_page: 'このページについて ${1} に質問する',
@@ -136,6 +136,8 @@ export const ko: TranslationLanguage = {
searched_for: '${1} 검색함',
ai_chat_tools_listed_pages: '문서를 탐색함',
ai_chat_tools_read_page: '${1} 페이지를 읽음',
ai_chat_tools_navigated_to_page: '페이지를 열었습니다',
ai_chat_tools_navigate_failed: '페이지를 열지 못했습니다',
ai_chat_tools_mcp_tool: '${1} 호출함',
ai_chat_ask: '${1}에게 질문',
ai_chat_ask_about_page: '이 페이지에 대해 ${1}에게 질문',
@@ -136,6 +136,8 @@ export const lt: TranslationLanguage = {
searched_for: 'Ieškota ${1}',
ai_chat_tools_listed_pages: 'Naršyta dokumentacija',
ai_chat_tools_read_page: 'Perskaitytas puslapis ${1}',
ai_chat_tools_navigated_to_page: 'Puslapis atidarytas',
ai_chat_tools_navigate_failed: 'Nepavyko atidaryti puslapio',
ai_chat_tools_mcp_tool: 'Iškviesta ${1}',
ai_chat_ask: 'Klausti ${1}',
ai_chat_ask_about_page: 'Klausti ${1} apie šį puslapį',
@@ -135,6 +135,8 @@ export const lv: TranslationLanguage = {
searched_for: 'Meklēts ${1}',
ai_chat_tools_listed_pages: 'Pārlūkoja dokumentāciju',
ai_chat_tools_read_page: 'Izlasīja lapu ${1}',
ai_chat_tools_navigated_to_page: 'Lapa atvērta',
ai_chat_tools_navigate_failed: 'Neizdevās atvērt lapu',
ai_chat_tools_mcp_tool: 'Izsauca ${1}',
ai_chat_ask: 'Jautāt ${1}',
ai_chat_ask_about_page: 'Jautāt ${1} par šo lapu',
@@ -136,6 +136,8 @@ export const ms: TranslationLanguage = {
searched_for: 'Mencari ${1}',
ai_chat_tools_listed_pages: 'Melayari dokumentasi',
ai_chat_tools_read_page: 'Membaca halaman ${1}',
ai_chat_tools_navigated_to_page: 'Halaman dibuka',
ai_chat_tools_navigate_failed: 'Gagal membuka halaman',
ai_chat_tools_mcp_tool: 'Memanggil ${1}',
ai_chat_ask: 'Tanya ${1}',
ai_chat_ask_about_page: 'Tanya ${1} tentang halaman ini',
@@ -137,6 +137,8 @@ export const nl: TranslationLanguage = {
searched_for: 'Gezocht naar ${1}',
ai_chat_tools_listed_pages: 'Docs doorzocht',
ai_chat_tools_read_page: 'Pagina ${1} gelezen',
ai_chat_tools_navigated_to_page: 'Pagina geopend',
ai_chat_tools_navigate_failed: 'Kan de pagina niet openen',
ai_chat_tools_mcp_tool: '${1} aangeroepen',
ai_chat_ask: 'Vraag het aan ${1}',
ai_chat_ask_about_page: 'Vraag ${1} naar deze pagina',
@@ -137,6 +137,8 @@ export const no: TranslationLanguage = {
searched_for: 'Søkte etter ${1}',
ai_chat_tools_listed_pages: 'Bladde gjennom docs',
ai_chat_tools_read_page: 'Leste side ${1}',
ai_chat_tools_navigated_to_page: 'Åpnet siden',
ai_chat_tools_navigate_failed: 'Kunne ikke åpne siden',
ai_chat_tools_mcp_tool: 'Kalte ${1}',
ai_chat_ask: 'Spør ${1}',
ai_chat_ask_about_page: 'Spør ${1} om denne siden',
@@ -136,6 +136,8 @@ export const pl: TranslationLanguage = {
searched_for: 'Wyszukano ${1}',
ai_chat_tools_listed_pages: 'Przeglądano dokumentację',
ai_chat_tools_read_page: 'Przeczytano stronę ${1}',
ai_chat_tools_navigated_to_page: 'Otwarto stronę',
ai_chat_tools_navigate_failed: 'Nie udało się otworzyć strony',
ai_chat_tools_mcp_tool: 'Wywołano ${1}',
ai_chat_ask: 'Zapytaj ${1}',
ai_chat_ask_about_page: 'Zapytaj ${1} o tę stronę',
@@ -137,6 +137,8 @@ export const pt_br = {
searched_for: 'Pesquisou por ${1}',
ai_chat_tools_listed_pages: 'Navegou pelos docs',
ai_chat_tools_read_page: 'Leu a página ${1}',
ai_chat_tools_navigated_to_page: 'Página aberta',
ai_chat_tools_navigate_failed: 'Falha ao abrir a página',
ai_chat_tools_mcp_tool: 'Chamou ${1}',
ai_chat_ask: 'Perguntar a ${1}',
ai_chat_ask_about_page: 'Perguntar a ${1} sobre esta página',
@@ -136,6 +136,8 @@ export const pt: TranslationLanguage = {
searched_for: 'Pesquisado por ${1}',
ai_chat_tools_listed_pages: 'Consultou a documentação',
ai_chat_tools_read_page: 'Leu a página ${1}',
ai_chat_tools_navigated_to_page: 'Página aberta',
ai_chat_tools_navigate_failed: 'Falha ao abrir a página',
ai_chat_tools_mcp_tool: 'Chamou ${1}',
ai_chat_ask: 'Perguntar a ${1}',
ai_chat_ask_about_page: 'Perguntar a ${1} sobre esta página',
@@ -138,6 +138,8 @@ export const ro: TranslationLanguage = {
searched_for: 'S-a căutat ${1}',
ai_chat_tools_listed_pages: 'A răsfoit documentația',
ai_chat_tools_read_page: 'A citit pagina ${1}',
ai_chat_tools_navigated_to_page: 'Pagina a fost deschisă',
ai_chat_tools_navigate_failed: 'Deschiderea paginii a eșuat',
ai_chat_tools_mcp_tool: 'A apelat ${1}',
ai_chat_ask: 'Întreabă ${1}',
ai_chat_ask_about_page: 'Întreabă ${1} despre această pagină',
@@ -136,6 +136,8 @@ export const ru = {
searched_for: 'Выполнен поиск ${1}',
ai_chat_tools_listed_pages: 'Просмотрены документы',
ai_chat_tools_read_page: 'Прочитана страница ${1}',
ai_chat_tools_navigated_to_page: 'Страница открыта',
ai_chat_tools_navigate_failed: 'Не удалось открыть страницу',
ai_chat_tools_mcp_tool: 'Вызван ${1}',
ai_chat_ask: 'Спросить у ${1}',
ai_chat_ask_about_page: 'Спросить у ${1} об этой странице',
@@ -137,6 +137,8 @@ export const sk: TranslationLanguage = {
searched_for: 'Hľadané ${1}',
ai_chat_tools_listed_pages: 'Prehliadal dokumentáciu',
ai_chat_tools_read_page: 'Prečítal stránku ${1}',
ai_chat_tools_navigated_to_page: 'Stránka otvorená',
ai_chat_tools_navigate_failed: 'Stránku sa nepodarilo otvoriť',
ai_chat_tools_mcp_tool: 'Zavolal ${1}',
ai_chat_ask: 'Opýtať sa ${1}',
ai_chat_ask_about_page: 'Opýtať sa ${1} na túto stránku',
@@ -136,6 +136,8 @@ export const sl: TranslationLanguage = {
searched_for: 'Iskano ${1}',
ai_chat_tools_listed_pages: 'Prebrskana dokumentacija',
ai_chat_tools_read_page: 'Prebrana stran ${1}',
ai_chat_tools_navigated_to_page: 'Stran odprta',
ai_chat_tools_navigate_failed: 'Strani ni bilo mogoče odpreti',
ai_chat_tools_mcp_tool: 'Poklicano ${1}',
ai_chat_ask: 'Vprašaj ${1}',
ai_chat_ask_about_page: 'Vprašaj ${1} o tej strani',
@@ -136,6 +136,8 @@ export const sv: TranslationLanguage = {
searched_for: 'Sökte efter ${1}',
ai_chat_tools_listed_pages: 'Bläddrade i dokumentationen',
ai_chat_tools_read_page: 'Läste sidan ${1}',
ai_chat_tools_navigated_to_page: 'Öppnade sidan',
ai_chat_tools_navigate_failed: 'Det gick inte att öppna sidan',
ai_chat_tools_mcp_tool: 'Anropade ${1}',
ai_chat_ask: 'Fråga ${1}',
ai_chat_ask_about_page: 'Fråga ${1} om den här sidan',
@@ -133,6 +133,8 @@ export const th: TranslationLanguage = {
searched_for: 'ค้นหา ${1}',
ai_chat_tools_listed_pages: 'เรียกดูเอกสาร',
ai_chat_tools_read_page: 'อ่านหน้า ${1}',
ai_chat_tools_navigated_to_page: 'เปิดหน้าแล้ว',
ai_chat_tools_navigate_failed: 'ไม่สามารถเปิดหน้าได้',
ai_chat_tools_mcp_tool: 'เรียกใช้ ${1}',
ai_chat_ask: 'ถาม ${1}',
ai_chat_ask_about_page: 'ถาม ${1} เกี่ยวกับหน้านี้',
@@ -135,6 +135,8 @@ export const tr: TranslationLanguage = {
searched_for: '${1} arandı',
ai_chat_tools_listed_pages: 'Dokümanlara göz atıldı',
ai_chat_tools_read_page: '${1} sayfası okundu',
ai_chat_tools_navigated_to_page: 'Sayfa açıldı',
ai_chat_tools_navigate_failed: 'Sayfa açılamadı',
ai_chat_tools_mcp_tool: '${1} çağrıldı',
ai_chat_ask: '${1} sor',
ai_chat_ask_about_page: 'Bu sayfa hakkında ${1} sor',
@@ -135,6 +135,8 @@ export const uk: TranslationLanguage = {
searched_for: 'Шукали ${1}',
ai_chat_tools_listed_pages: 'Переглянуто документацію',
ai_chat_tools_read_page: 'Прочитано сторінку ${1}',
ai_chat_tools_navigated_to_page: 'Сторінку відкрито',
ai_chat_tools_navigate_failed: 'Не вдалося відкрити сторінку',
ai_chat_tools_mcp_tool: 'Викликано ${1}',
ai_chat_ask: 'Запитати ${1}',
ai_chat_ask_about_page: 'Запитати ${1} про цю сторінку',
@@ -135,6 +135,8 @@ export const vi: TranslationLanguage = {
searched_for: 'Đã tìm kiếm ${1}',
ai_chat_tools_listed_pages: 'Đã duyệt tài liệu',
ai_chat_tools_read_page: 'Đã đọc trang ${1}',
ai_chat_tools_navigated_to_page: 'Đã mở trang',
ai_chat_tools_navigate_failed: 'Không thể mở trang',
ai_chat_tools_mcp_tool: 'Đã gọi ${1}',
ai_chat_ask: 'Hỏi ${1}',
ai_chat_ask_about_page: 'Hỏi ${1} về trang này',
@@ -132,6 +132,8 @@ export const yue: TranslationLanguage = {
searched_for: '已搜尋 ${1}',
ai_chat_tools_listed_pages: '瀏覽咗文件',
ai_chat_tools_read_page: '已閱讀頁面 ${1}',
ai_chat_tools_navigated_to_page: '已開啟頁面',
ai_chat_tools_navigate_failed: '無法開啟頁面',
ai_chat_tools_mcp_tool: '已呼叫 ${1}',
ai_chat_ask: '問 ${1}',
ai_chat_ask_about_page: '問 ${1} 關於此頁面',
@@ -132,6 +132,8 @@ export const zh_tw: TranslationLanguage = {
searched_for: '已搜尋 ${1}',
ai_chat_tools_listed_pages: '瀏覽了文件',
ai_chat_tools_read_page: '已閱讀頁面 ${1}',
ai_chat_tools_navigated_to_page: '已開啟頁面',
ai_chat_tools_navigate_failed: '無法開啟頁面',
ai_chat_tools_mcp_tool: '已呼叫 ${1}',
ai_chat_ask: '詢問 ${1}',
ai_chat_ask_about_page: '詢問 ${1} 關於此頁面',
@@ -133,6 +133,8 @@ export const zh: TranslationLanguage = {
searched_for: '搜索了 ${1}',
ai_chat_tools_listed_pages: '浏览了文档',
ai_chat_tools_read_page: '已读取页面 ${1}',
ai_chat_tools_navigated_to_page: '已打开页面',
ai_chat_tools_navigate_failed: '无法打开页面',
ai_chat_tools_mcp_tool: '调用了 ${1}',
ai_chat_ask: '向 ${1} 提问',
ai_chat_ask_about_page: '向 ${1} 提问有关此页面的问题',