From ada1dce73920c77a610a44c574f4562bc07ce867 Mon Sep 17 00:00:00 2001 From: claude code agent 227 Date: Sat, 20 Jun 2026 21:57:03 +0300 Subject: [PATCH 1/2] fix(ai-chat): resolve the current page for agent context (#43, hardness #1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AiChatWindow derived the open page via useParams(), but it's mounted in a pathless parent layout route where :pageSlug isn't matched, so useParams() returned {} and openPage was ALWAYS null — the agent never received current-page context (couldn't resolve 'this page'/'the current page'). Derive pageSlug from useMatch('/s/:spaceSlug/p/:pageSlug') against the full pathname instead, so it resolves regardless of where the component sits in the route tree. No-match behavior is unchanged (undefined -> query disabled -> openPage null). Addresses Hardness #1 of #43. Hardness #2 (proxy resilience: a get_current_page tool / hidden user-message context so identity doesn't depend on the system prompt surviving CLIProxyAPI) remains open. Co-Authored-By: Claude Opus 4.8 --- .../ai-chat/components/ai-chat-window.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/client/src/features/ai-chat/components/ai-chat-window.tsx b/apps/client/src/features/ai-chat/components/ai-chat-window.tsx index 1b9012c5..8e6258f3 100644 --- a/apps/client/src/features/ai-chat/components/ai-chat-window.tsx +++ b/apps/client/src/features/ai-chat/components/ai-chat-window.tsx @@ -18,7 +18,7 @@ import { IconX, } from "@tabler/icons-react"; import { useAtom, useSetAtom } from "jotai"; -import { useParams } from "react-router-dom"; +import { useMatch } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { useQueryClient } from "@tanstack/react-query"; import { @@ -140,13 +140,16 @@ export default function AiChatWindow() { const { data: messageRows, isLoading: messagesLoading } = useAiChatMessagesQuery(activeChatId ?? undefined); - // The page the user is currently viewing, derived from the route (same - // source the breadcrumb uses). On a non-page route `pageSlug` is undefined, - // so the query is disabled and `openPage` is null. This is passed to the - // chat thread as context so the agent knows what "this page"/"the current - // page" refers to; the agent still reads/writes via its CASL-enforced page - // tools using the id. - const { pageSlug } = useParams(); + // The page the user is currently viewing. AiChatWindow lives in a pathless + // parent layout route, so useParams() can't see :pageSlug. Match the full + // pathname against the authenticated page route instead so "the current page" + // resolves regardless of where this component is mounted. On a non-page route + // the match is null, so `pageSlug` is undefined, the query is disabled and + // `openPage` is null. This is passed to the chat thread as context so the + // agent knows what "this page"/"the current page" refers to; the agent still + // reads/writes via its CASL-enforced page tools using the id. + const pageRouteMatch = useMatch("/s/:spaceSlug/p/:pageSlug"); + const pageSlug = pageRouteMatch?.params?.pageSlug; const { data: openPageData } = usePageQuery({ pageId: extractPageSlugId(pageSlug), }); From a6ba19f0dc1de104295ed70e29f83d142597f257 Mon Sep 17 00:00:00 2001 From: claude code agent 227 Date: Sat, 20 Jun 2026 22:19:40 +0300 Subject: [PATCH 2/2] feat(ai-chat): add get_current_page tool for proxy-robust page context (#43, hardness #2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The current page id was only injected as text in the system prompt, which a proxy (CLIProxyAPI) can rewrite/truncate, so the agent could lose track of 'this page'. Add a getCurrentPage tool the model can call to read the open page (id + title) from the server-side request context (forUser now takes openedPage, threaded from body.openPage — the same value used for the system prompt). The inline system-prompt line is kept as belt-and-suspenders. Reads/writes still go through the CASL-enforced page tools by id, so this is strictly not worse than the existing prompt hint — just delivered over a channel the proxy can't mangle. User-approved on the issue. Completes #43 together with the hardness-1 fix. Co-Authored-By: Claude Opus 4.8 --- .../src/core/ai-chat/ai-chat.service.ts | 3 +++ .../ai-chat/tools/ai-chat-tools.service.ts | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/apps/server/src/core/ai-chat/ai-chat.service.ts b/apps/server/src/core/ai-chat/ai-chat.service.ts index 4c4bc6f4..1c671bb0 100644 --- a/apps/server/src/core/ai-chat/ai-chat.service.ts +++ b/apps/server/src/core/ai-chat/ai-chat.service.ts @@ -257,6 +257,9 @@ export class AiChatService { sessionId, workspace.id, chatId, + // Same open-page value used by the system prompt above; exposed to the + // model via getCurrentPage so page identity survives prompt mangling. + body.openPage, ); // Merge in admin-configured external MCP tools (web search, etc.; §6.8). diff --git a/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts b/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts index ef7dae56..038e2544 100644 --- a/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts +++ b/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts @@ -50,6 +50,11 @@ export class AiChatToolsService { // agent write (REST + collab) records { actor:'agent', aiChatId } off a // SIGNED claim — non-spoofable, never a client body field (§6.5/§6.6). aiChatId: string, + // The page the user currently has open (from the request context), exposed + // to the model via getCurrentPage. Optional and last so existing callers + // keep compiling. Kept proxy-robust: the model can CALL for the current + // page instead of relying on it surviving in the system prompt text. + openedPage?: { id?: string; title?: string } | null, ): Promise> { const apiUrl = process.env.MCP_DOCMOST_API_URL || @@ -210,6 +215,23 @@ export class AiChatToolsService { }, }), + getCurrentPage: tool({ + description: + 'Return the page the user is currently viewing — i.e. what "this page", ' + + '"the current page", or "here" refers to. Returns the page id and title, ' + + 'or null if the user is not currently on a page. Call this first whenever ' + + 'the user refers to the current page without giving an explicit id.', + inputSchema: z.object({}), + execute: async () => { + if (!openedPage?.id) { + return { page: null }; + } + return { + page: { id: openedPage.id, title: openedPage.title ?? '' }, + }; + }, + }), + getPage: tool({ description: 'Fetch a single page as Markdown by its page id. Returns the page ' +