📌 feat: Seed Default Pinned Tools and MCP Dropdown via Interface Config#13865
Conversation
…MCP pinning Adds an `interface.defaultPinnedTools` string array letting admins pin tools and the MCP servers dropdown to the prompt bar by default for all users. - Tool keys (artifacts, execute_code, web_search, file_search, skills) pin their badge via `useToolToggle`. - The keyword `'mcp'` or a configured MCP server name pins the MCP dropdown via `useMCPSelect`. - Only seeds initial state; a user's stored pin preference always wins. When unset, tools start unpinned and the MCP dropdown keeps its legacy default (pinned). Unifies the approaches from #11646 (pinnedTools) and #9251 (defaultPinMcp) into one config key.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 761379bbf5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const [isPinned, setIsPinned] = useLocalStorage<boolean>( | ||
| `${localStorageKey}pinned`, | ||
| defaultPinned, | ||
| ); |
There was a problem hiding this comment.
Migrate existing auto-seeded unpinned values
For users who have loaded the app before this feature, ${localStorageKey}pinned already exists as false because the previous hook call used useLocalStorage(..., false) and useLocalStorageAlt writes the default when the key is absent. Passing defaultPinned only as the hook's initial default means those auto-created false values are treated as user preferences, so an admin setting interface.defaultPinnedTools: ['execute_code'] will not pin Code Interpreter/Web/File/etc. for existing users who never chose a pin state. Add a migration or a way to distinguish auto-seeded defaults from explicit user choices before honoring the stored value.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Thanks — this is accurate, and it's an intentional design choice here rather than a bug.
defaultPinnedTools is scoped to seed the initial pin state only; it deliberately does not override a value already persisted on the client. That matches how the rest of LibreChat's interface defaults behave (we don't retroactively rewrite a user's localStorage-persisted UI state) and the intended semantics of this option: it "only affects the initial/default state and doesn't override user preferences once they've interacted with pinning."
The constraint you've identified is real: in the pre-existing data model an auto-seeded false is indistinguishable from a deliberate unpin. So any migration that force-applies the configured default to existing false values would also re-pin tools that users intentionally unpinned — a worse regression than the gap it closes. The maintainer's call is to not override existing client state.
Net effect: the defaults apply to new users and fresh storage; existing users keep their current pins and can still pin manually as today. If broad retroactive application is ever wanted, it would be a separate, versioned pin-storage migration rather than something folded into this PR.
defaultPinnedTools interface config for default tool & MCP pinning|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 761379bbf5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const [isPinned, setIsPinned] = useLocalStorage<boolean>( | ||
| `${localStorageKey}pinned`, | ||
| defaultPinned, | ||
| ); |
There was a problem hiding this comment.
Defer pin initialization until startup config loads
In a cold load where useGetStartupConfig() is still fetching, defaultPinned is false and useLocalStorageAlt persists that default when the pin key is absent; its initialization effect also does not rerun for later defaultValue changes. When the config eventually returns with defaultPinnedTools: ['execute_code'] (or any other tool), the stored false is now treated as the user's preference, so the admin-configured default pin is missed for fresh users whenever startup config was not already cached.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in e180cc3.
useToolToggle now captures whether a pin preference already existed before mount (pre-seed), and a one-shot effect applies the configured default once startupConfig resolves — for users with no prior preference only. So on a cold load where useGetStartupConfig() is still fetching, the eagerly-seeded false is corrected to the admin default when the config arrives, while an existing stored pin is never overridden (keeping the conservative behavior for returning users).
For reference, the MCP path in useMCPSelect was already guarded this way: its init effect early-returns until startupConfig is present, and mcpPinnedAtom (atomWithStorage) doesn't eager-seed, so absence of a stored value stays meaningful.
Added an integration test (useToolToggle.pinDefault.test.tsx, using the real useLocalStorageAlt + jsdom localStorage) covering: the cold-load race (config resolves after mount → default applied), the already-cached path, and the no-override guarantee for an existing stored unpin.
On a cold load, useToolToggle can mount before useGetStartupConfig() resolves, so defaultPinned starts false and useLocalStorageAlt eagerly persists it; its init effect never re-runs for the later config-driven default. Fresh users would then miss the admin-configured default pin whenever startup config was not already cached. Capture whether a pin preference existed before mount (pre-seed) and, once startupConfig arrives, apply the real default for users with no prior preference. Runs once and never overrides an existing stored pin, so the conservative behavior for existing users is preserved.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e180cc30be
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (hadStoredPin) { | ||
| return; | ||
| } | ||
| if (defaultPinned !== isPinned) { | ||
| setIsPinned(defaultPinned); |
There was a problem hiding this comment.
Preserve pin clicks made before startup config loads
When startupConfig is still null on the first render, a user can toggle a tool's pin before the query resolves. Because hadStoredPin is captured only once at mount, this block still treats the user as having no preference and calls setIsPinned(defaultPinned) when config arrives, overwriting that click (for example, a manually pinned code tool becomes unpinned if it is not listed). Re-check the current storage value or mark user updates before applying the admin default.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in fb9a086.
Explicit pin toggles now flip a ref (set through the returned setter), and the admin-default effect skips application whenever the user has interacted in-session — alongside the existing stored-preference guard. So a pin clicked during the cold-load window (before startupConfig resolves) is preserved instead of being overwritten by the default.
Added a test for exactly this case: pin the tool before config resolves, then have config arrive without the tool listed — the user's pin is kept (useToolToggle.pinDefault.test.tsx).
|
|
||
| /** Captured before mount (pre-seed): did the user already have a stored pin preference? | ||
| * Distinguishes a real choice from `useLocalStorage`'s eager default-seed below. */ | ||
| const [hadStoredPin] = useState(() => localStorage.getItem(`${localStorageKey}pinned`) != null); |
There was a problem hiding this comment.
Don't treat old auto-seeded pin values as user choices
On upgraded clients that have previously visited chat, useLocalStorageAlt has already written ${localStorageKey}pinned with false even when the user never touched the pin. Capturing only key presence here makes hadStoredPin true for those legacy auto-seeded values, so an admin setting defaultPinnedTools: ['execute_code'] will be ignored for most existing users. Use an explicit user-action marker or migrate/remove the old default seed before deciding the user has a stored preference.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This is the same behavior discussed on the earlier thread about auto-seeded values, and it's intentional rather than a bug to fix in this PR.
defaultPinnedTools deliberately seeds the initial state only and does not override a value already persisted on the client. On upgraded clients, the legacy auto-seeded ${localStorageKey}pinned is treated as the user's existing state and respected, so the option applies to new users and fresh storage.
The key constraint is that an auto-seeded false and a deliberate unpin are indistinguishable in the pre-existing data model. A migration that force-applied the configured default to legacy false values would therefore also re-pin tools that users intentionally unpinned — a worse regression than the gap it closes. The maintainer's decision is to keep this conservative (don't override existing client state); broad retroactive application, if ever wanted, would be a separate versioned pin-storage migration.
The cold-load default-seeding effect captured the stored-pin state only at mount, so a pin toggled before startupConfig resolved was treated as no-preference and overwritten when the admin default applied. Track explicit pin toggles via a ref (set through the returned setter) and skip the default application when the user has interacted in-session — in addition to the existing stored-preference guard.
|
@codex review |
|
Codex Review: Didn't find any major issues. Delightful! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
✨ feat: Add
defaultPinnedToolsinterface config for default tool & MCP pinning📋 Summary
Adds a single
interface.defaultPinnedToolsstring array that lets admins control which tools — and the MCP servers dropdown — are pinned to the prompt bar by default for all users.This unifies two prior community contributions into one config key:
pinnedToolsarray for tool pinning)defaultPinMcpboolean for MCP dropdown pinning) — now folded in via the'mcp'sentinel, per the discussion on those PRs.🎯 Problem
Users currently have to manually pin each tool (Artifacts, Code Interpreter, Web Search, etc.) one-by-one, every environment, and the MCP dropdown's default pinned state isn't configurable. Admins had no way to set sensible defaults org-wide.
✨ Solution
artifacts,execute_code,web_search,file_search,skills) pins that badge by default, wired throughuseToolToggle.'mcp'(or a specific configured MCP server name) pins the MCP servers dropdown, wired throughuseMCPSelect.default*options behave).'mcp'is how an admin unpins MCP by default (the olddefaultPinMcp: falseuse case).How it flows
interface.defaultPinnedTools→interfaceSchema(data-provider) →loadDefaultInterface(data-schemas) →startupConfig.interface→ consumed byuseToolToggle/useMCPSelecton the client.🧪 Testing
packages/data-providerconfig-schemas.spec.ts— schema accepts the array / leaves it undefined when unset.packages/data-schemasinterface.spec.ts— passthrough when configured, omitted when not.clientuseToolToggle.test.tsx— seeds pinned state from the configured tool keys.clientuseMCPSelect.test.tsx—'mcp'/server-name pins, configured-without-mcp unpins, legacy default preserved, stored user preference wins.All affected suites pass;
eslintand clienttsc --noEmitare clean.Change Type
Checklist
librechat.aiwill follow