Skip to content

📌 feat: Add Pin Support for Model Specs#11219

Merged
danny-avila merged 7 commits into
devfrom
feat/pin-model-specs
Apr 9, 2026
Merged

📌 feat: Add Pin Support for Model Specs#11219
danny-avila merged 7 commits into
devfrom
feat/pin-model-specs

Conversation

@berry-13

@berry-13 berry-13 commented Jan 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds the ability to pin/favorite model specs, similar to existing support for models and agents
  • Reduces padding on submenu dropdowns for a more compact look

Changes

Frontend:

  • Added spec field to Favorite type
  • Added addFavoriteSpec, removeFavoriteSpec, isFavoriteSpec, toggleFavoriteSpec methods to useFavorites hook
  • Added pin button to ModelSpecItem component (shows on hover)
  • Updated FavoriteItem and FavoritesList to render pinned specs
  • Reduced submenu padding from px-3 py-2 to px-0.5 py-0.5

Backend:

  • Updated FavoritesController validation to accept spec as a valid favorite type
  • Ensures mutual exclusivity between agentId, model+endpoint, and spec

Data Provider:

  • Added spec field to FavoriteItem type
  • Bump version to 0.8.211

Change Type

  • Style
  • New feature (non-breaking change which adds functionality)

Testing

  • Configure model specs in librechat.yaml
  • Hover over a model spec in the model selector and click the pin icon
  • Verify the spec appears in the sidebar favorites
  • Verify clicking a pinned spec selects it
  • Verify unpinning works via the dropdown menu
  • Verify drag-and-drop reordering works with mixed favorites (agents, models, specs)

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes
  • Any changes dependent on mine have been merged and published in downstream modules.

@berry-13 berry-13 added ✨ enhancement New feature or request 🎨 design UI/UX improvements labels Jan 5, 2026
@berry-13 berry-13 force-pushed the feat/pin-model-specs branch from 075546b to 9ea62ad Compare February 12, 2026 18:29
@berry-13 berry-13 linked an issue Apr 2, 2026 that may be closed by this pull request
1 task
@berry-13 berry-13 changed the base branch from main to dev April 8, 2026 22:24
@berry-13 berry-13 force-pushed the feat/pin-model-specs branch from 9ea62ad to bfeefb9 Compare April 9, 2026 15:09
Copilot AI review requested due to automatic review settings April 9, 2026 15:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds “pin/favorite” support for model specs end-to-end (schema + API validation + client state/UI), and tightens up submenu padding for a more compact nested menu layout.

Changes:

  • Extend favorites data model to support { spec: string } entries and validate them server-side.
  • Update client favorites hook + sidebar favorites rendering to include pinned model specs.
  • Add a reusable useIsActiveItem hook to improve keyboard/focus behavior for pin buttons inside Ariakit composite menus; adjust submenu padding styles.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/data-schemas/src/types/user.ts Align IUser.favorites typing with shared TUserFavorite.
packages/data-schemas/src/schema/user.ts Add spec to favorites schema and apply maxlength constraints.
packages/data-provider/src/types/queries.ts Introduce exported TUserFavorite type (agent/model/spec variants).
packages/data-provider/src/data-service.ts Switch favorites API typings to use TUserFavorite.
client/src/store/favorites.ts Reuse TUserFavorite for client favorite state type.
client/src/locales/en/translation.json Add com_ui_model_spec label for spec favorites.
client/src/hooks/useIsActiveItem.ts New hook to mirror Ariakit data-active-item into React state.
client/src/hooks/index.ts Export useIsActiveItem.
client/src/hooks/tests/useIsActiveItem.spec.tsx Add unit tests for useIsActiveItem.
client/src/hooks/useFavorites.ts Add spec favorite helpers + clean/normalize favorites to canonical shapes.
client/src/hooks/tests/useFavorites.spec.tsx Add tests for spec favorite methods and cleaning behavior.
client/src/components/Nav/Favorites/FavoritesList.tsx Render spec favorites by resolving pinned spec names from startupConfig.
client/src/components/Nav/Favorites/FavoriteItem.tsx Support rendering/selecting/removing spec favorites in sidebar items.
client/src/components/Nav/Favorites/tests/FavoritesList.spec.tsx Add tests for spec rendering in the favorites list.
client/src/components/Nav/Favorites/tests/FavoriteItem.spec.tsx Add tests for spec-type favorite item behavior and a11y label.
client/src/components/Chat/Menus/Endpoints/CustomMenu.tsx Reduce padding for nested (submenu) dropdowns.
client/src/components/Chat/Menus/Endpoints/components/ModelSpecItem.tsx Add hover/active pin button for model spec items.
client/src/components/Chat/Menus/Endpoints/components/EndpointModelItem.tsx Replace inline MutationObserver with useIsActiveItem and improve pin button focus styles.
client/src/components/Chat/Menus/Endpoints/components/tests/ModelSpecItem.test.tsx Add tests for the model spec pin button and interactions.
client/src/components/Chat/Menus/Endpoints/components/tests/EndpointModelItem.test.tsx Update test mocks to include useIsActiveItem.
api/server/controllers/FavoritesController.js Accept/validate spec favorites and enforce (partial) exclusivity rules.
api/server/controllers/FavoritesController.spec.js Add backend controller test coverage for spec + exclusivity validation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 27 to 69
@@ -43,16 +44,27 @@ const updateFavoritesController = async (req, res) => {
.status(400)
.json({ message: `endpoint exceeds maximum length of ${MAX_STRING_LENGTH}` });
}
if (fav.spec !== undefined && fav.spec !== null) {
if (typeof fav.spec !== 'string' || fav.spec.length === 0) {
return res.status(400).json({ message: 'spec must be a non-empty string' });
}
if (fav.spec.length > MAX_STRING_LENGTH) {
return res
.status(400)
.json({ message: `spec exceeds maximum length of ${MAX_STRING_LENGTH}` });
}
}

if (!hasAgent && !hasModel) {
const typeCount = [hasAgent, hasModel, hasSpec].filter(Boolean).length;
if (typeCount === 0) {
return res.status(400).json({
message: 'Each favorite must have either agentId or model+endpoint',
message: 'Each favorite must have either agentId, model+endpoint, or spec',
});
}

if (hasAgent && hasModel) {
if (typeCount > 1) {
return res.status(400).json({
message: 'Favorite cannot have both agentId and model/endpoint',
message: 'Favorite cannot have multiple types (agentId, model/endpoint, or spec)',
});
}

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mutual-exclusivity logic only counts the model variant when both model and endpoint are set. This allows mixed/invalid entries like { agentId, model } or { spec, endpoint } to pass validation and be persisted with multiple type fields present. Consider rejecting when any non-null field from another variant is present, and/or explicitly requiring model and endpoint together when either is provided (and rejecting partials).

Copilot uses AI. Check for mistakes.
Comment on lines +396 to +400
} else if (fav.spec) {
const spec = specsMap[fav.spec];
if (!spec) {
return null;
}

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a spec favorite is stale (missing from startupConfig.modelSpecs.list), this returns null but the entry remains in safeFavorites. That leaves index “holes” in the rendered list (and in react-dnd indices), which can break drag/drop ordering and makes the stale favorite impossible to remove via the UI. Consider filtering/cleaning spec favorites that aren’t present in specsMap (similar to the stale agent cleanup) before rendering and before DnD indexing/persistence.

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +60
<button
tabIndex={isActive ? 0 : -1}
onClick={handleFavoriteClick}
aria-label={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')}

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pin/unpin control is a plain <button> without an explicit type. If this menu is ever rendered inside a <form>, the button will default to type="submit" and can cause unintended submissions. Set type="button" here (and in the similar pin button in EndpointModelItem).

Copilot uses AI. Check for mistakes.
Comment on lines 111 to 115
<button
tabIndex={isActive ? 0 : -1}
onClick={handleFavoriteClick}
aria-label={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')}
className={cn(

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pin/unpin control is a plain <button> without an explicit type. If this menu is ever rendered inside a <form>, the button will default to type="submit" and can cause unintended submissions. Set type="button" here (and in ModelSpecItem).

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +44
it('initializes isActive=true when data-active-item is already set pre-mount', () => {
// Render wrapper that sets the attribute BEFORE Probe mounts.
function Wrapper() {
const [mounted, setMounted] = React.useState(false);
return (
<div>
{mounted && <Probe />}
<button
data-testid="mount-trigger"
onClick={() => {
setMounted(true);
}}
/>
</div>
);
}
// Simplest: assert default is false (initial mount has no attribute); the pre-mount
// hydration case is effectively the same code path covered by the update test below.
const { container } = render(<Wrapper />);
// Not strictly asserting here — initial-attribute path is exercised below via mutation.
expect(container).toBeTruthy();
});

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case is named as if it verifies the pre-mount data-active-item initialization behavior, but it doesn’t assert anything about isActive (it only checks container truthiness). Either implement a real assertion for the pre-mount attribute scenario or remove/rename this test to avoid giving a false sense of coverage.

Copilot uses AI. Check for mistakes.
Comment on lines +205 to +215
/**
* User Favorites — pinned agents, models, and model specs.
* Exactly one variant should be set per entry; exclusivity is enforced
* server-side in FavoritesController. Shape is loose for state-update ergonomics.
*/
export type TUserFavorite = {
agentId?: string;
model?: string;
endpoint?: string;
spec?: string;
};

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions bumping the data-provider version to 0.8.211, but this change set only adds TUserFavorite and doesn’t include a version bump (and package.json currently shows a different version). Either update the PR description or include the intended version bump so downstream consumers can rely on the new exported type.

Copilot uses AI. Check for mistakes.
@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bfeefb9878

ℹ️ 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".

Comment on lines +398 to +399
if (!spec) {
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Purge missing spec favorites instead of silently skipping

When a persisted favorite points to a spec name that is no longer present in startupConfig, this branch returns null and leaves the stale entry in state/storage. That makes the item impossible to unpin from the UI, still counts toward the 50-favorite limit, and can leave an empty favorites section mounted because favorites.length stays nonzero. This should be cleaned up the same way stale agent favorites are cleaned up (via reorderFavorites(..., true)) instead of only hiding it at render time.

Useful? React with 👍 / 👎.

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 543a2f66a4

ℹ️ 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 (!staleSpecNamesKey || specCleanupAttemptedRef.current === staleSpecNamesKey) {
return;
}
const staleSet = new Set(staleSpecNamesKey.split(','));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop splitting stale spec keys on raw commas

Using staleSpecNamesKey.split(',') assumes spec names never contain commas, but model spec names are free-form strings, so valid names like "foo,bar" get corrupted during cleanup. In that case the stale spec is not removed (because "foo,bar" is split into "foo"/"bar"), and unrelated favorites with those fragment names can be removed instead, causing incorrect persistence when reorderFavorites(cleaned, true) runs.

Useful? React with 👍 / 👎.

@danny-avila danny-avila changed the title 📌 feat: add pin support for model specs 📌 feat: Add Pin Support for Model Specs Apr 9, 2026
berry-13 and others added 7 commits April 9, 2026 17:47
- Add canonical TUserFavorite type in data-provider, replace three
  duplicate definitions (data-service, data-schemas, store/favorites)
- Consolidate FavoritesController spec validation into single block,
  add return on 500 paths, add maxlength to mongoose sub-schema
- Fix import order in FavoritesList.tsx, merge namespace type imports
  in FavoriteItem.tsx and FavoritesList.tsx
- Add focus-visible:ring-inset on pin buttons to prevent ring clipping
- Add explicit return type and JSDoc on useIsActiveItem hook
- Use props.type for narrowing consistency in FavoriteItem getTypeLabel
- Add 22 backend tests for FavoritesController (spec validation,
  typeCount exclusivity, persistence, GET path)
- Add 40 frontend tests: useFavorites spec methods, useIsActiveItem
  observer lifecycle, ModelSpecItem pin button, FavoriteItem all three
  type branches, FavoritesList spec rendering
- Harden backend validation to reject partial cross-type fields
  (e.g. spec+endpoint, agentId+model without endpoint)
- Add stale-spec auto-cleanup in FavoritesList mirroring agent cleanup
- Add type="button" to pin buttons in ModelSpecItem/EndpointModelItem
- Fix import order violations in EndpointModelItem and ModelSpecItem
- Remove hollow test, dead key prop, inline trivial helpers
- Fix misleading test description, add onSelectSpec to test mock
- Add return to controller success responses for consistency
- Add 6 backend tests for partial cross-type field validation
Prevents race condition where spec favorites are incorrectly deleted
on cold start before startupConfig has loaded. Mirrors the existing
agentsMap === undefined guard pattern used for stale agent cleanup.

Also adds tests for stale-spec cleanup persistence and fixes namespace
import pattern in FavoritesList.spec.tsx.
Resolves ESLint no-nested-ternary warnings for name and typeLabel
derivations.
@danny-avila danny-avila force-pushed the feat/pin-model-specs branch from b824314 to ee9380a Compare April 9, 2026 21:48
@danny-avila danny-avila merged commit 1a83f36 into dev Apr 9, 2026
15 checks passed
@danny-avila danny-avila deleted the feat/pin-model-specs branch April 9, 2026 22:37
jcbartle pushed a commit to jcbartle/LibreChat that referenced this pull request May 11, 2026
* feat: Enhance favorites functionality to support model specs

* refactor(FavoritesList): reorder imports based on lenght

* feat: improve favorite modelSpec controller; refactor: useIsActiveItem hook

* refactor: consolidate Favorite type, harden controller, add tests

- Add canonical TUserFavorite type in data-provider, replace three
  duplicate definitions (data-service, data-schemas, store/favorites)
- Consolidate FavoritesController spec validation into single block,
  add return on 500 paths, add maxlength to mongoose sub-schema
- Fix import order in FavoritesList.tsx, merge namespace type imports
  in FavoriteItem.tsx and FavoritesList.tsx
- Add focus-visible:ring-inset on pin buttons to prevent ring clipping
- Add explicit return type and JSDoc on useIsActiveItem hook
- Use props.type for narrowing consistency in FavoriteItem getTypeLabel
- Add 22 backend tests for FavoritesController (spec validation,
  typeCount exclusivity, persistence, GET path)
- Add 40 frontend tests: useFavorites spec methods, useIsActiveItem
  observer lifecycle, ModelSpecItem pin button, FavoriteItem all three
  type branches, FavoritesList spec rendering

* fix: address PR review findings for pin model specs

- Harden backend validation to reject partial cross-type fields
  (e.g. spec+endpoint, agentId+model without endpoint)
- Add stale-spec auto-cleanup in FavoritesList mirroring agent cleanup
- Add type="button" to pin buttons in ModelSpecItem/EndpointModelItem
- Fix import order violations in EndpointModelItem and ModelSpecItem
- Remove hollow test, dead key prop, inline trivial helpers
- Fix misleading test description, add onSelectSpec to test mock
- Add return to controller success responses for consistency
- Add 6 backend tests for partial cross-type field validation

* fix: guard stale-spec cleanup against unloaded startupConfig

Prevents race condition where spec favorites are incorrectly deleted
on cold start before startupConfig has loaded. Mirrors the existing
agentsMap === undefined guard pattern used for stale agent cleanup.

Also adds tests for stale-spec cleanup persistence and fixes namespace
import pattern in FavoritesList.spec.tsx.

* fix: replace nested ternaries with if/else in FavoriteItem

Resolves ESLint no-nested-ternary warnings for name and typeLabel
derivations.

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🎨 design UI/UX improvements ✨ enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enhancement]: make modelSpec.list pinnable

3 participants