From 14445edbd3ef52bbc95998f865b9e2d57662dfc9 Mon Sep 17 00:00:00 2001 From: AlexRV12 <71396855+AlexRV12@users.noreply.github.com> Date: Wed, 9 Sep 2026 14:44:32 +0200 Subject: [PATCH] fix: count plan-mode tools in the shipped toolset Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L99mAR4LitqTcYY1Kn1ATH --- .../copilot/chat/AIChatManager.svelte.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index 07dcb0b47c..bd69e793cd 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -656,10 +656,13 @@ export class AIChatManager { content: '' }) tools = $state[]>([]) - /** What the request actually carries: `tools` minus whatever this session's - * capabilities withhold. Anything describing the toolset to the user or counting - * its cost must read this, not `tools`. */ - shippedTools = $derived(filterSessionTools(this.tools, this.sessionAccess)) + /** Every tool source converged and capability-filtered — what the request carries, + * so anything describing the toolset or counting its cost reads this, not `tools`. + * One array both advertises the tools and dispatches the calls, which is what makes + * a withheld tool unreachable rather than merely unlisted. */ + shippedTools = $derived( + filterSessionTools([...this.tools, ...this.planMode.tools], this.sessionAccess) + ) helpers = $state(undefined) scriptEditorOptions = $state(undefined) @@ -2032,8 +2035,10 @@ export class AIChatManager { } ) { if (!isAIModeVisible(mode)) return - // A session chat is GLOBAL for its whole life, and the plan gate reads that mode: moving - // it lifts the gate on a session the user still has set to Plan. + // A session chat is GLOBAL for its whole life, and two things read that mode: moving it + // lifts the plan gate on a session the user still has set to Plan, and leaves + // `shippedTools` filtering a toolset whose names have no policy entries, so it fails + // closed to nothing. if (this.isSessionChat && mode !== AIMode.GLOBAL) { console.error(`Refusing to move a session chat to ${mode} mode: sessions are GLOBAL-only.`) return @@ -2688,15 +2693,8 @@ export class AIChatManager { base = self.planMode.decorateSystemMessage(base) return base }, - // The one place every tool source converges, which is why the capability - // filter belongs here and not in `globalToolsFor`. This same array both - // advertises the tools and dispatches the calls, so a withheld tool is - // unreachable rather than merely unlisted. Filtering is unconditional - // because `sessionAccess` is set only for session chats and `changeMode` - // keeps those GLOBAL — a non-GLOBAL toolset has no policy entries and - // would fail closed to nothing. get tools() { - return filterSessionTools([...self.tools, ...self.planMode.tools], self.sessionAccess) + return self.shippedTools }, get helpers() { return self.helpers