Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
644b552
🧬 feat: Persist `disable-model-invocation` / `user-invocable` / `allo…
danny-avila Apr 19, 2026
0d2f03a
πŸ›‘οΈ feat: Enforce frontmatter at runtime (catalog, skill tool, manual …
danny-avila Apr 19, 2026
8dfdb3d
πŸŽ›οΈ feat: `$` popover reads `userInvocable` instead of UI-only `invoca…
danny-avila Apr 19, 2026
fbd0021
πŸ”§ fix: Address Phase 6 review findings
danny-avila Apr 19, 2026
ea09553
πŸ”§ fix: Address codex iter 2 β€” catalog quota + duplicate-name dedup
danny-avila Apr 19, 2026
cc8d820
πŸ”’ fix: Apply `disable-model-invocation` gate to read_file too (codex …
danny-avila Apr 19, 2026
0b00cc7
πŸ”§ fix: Address codex iter 4 β€” manual-prime exception + legacy frontma…
danny-avila Apr 19, 2026
530cf6f
πŸ”§ fix: Address codex iter 5 β€” propagate manualSkillNames + keep read_…
danny-avila Apr 19, 2026
09a20d1
πŸ”§ fix: Address codex iter 6 β€” name-collision consistency via preferIn…
danny-avila Apr 19, 2026
45d5c38
πŸ”§ fix: Address codex iter 7 β€” split preferInvocable into per-axis flags
danny-avila Apr 19, 2026
11b17bc
πŸ”§ fix: TypeScript type-check failure in handlers.spec.ts (CI green)
danny-avila Apr 19, 2026
3cb9ead
πŸ”§ fix: Address codex iter 8 β€” undefined-result fallback + read_file a…
danny-avila Apr 19, 2026
aea9f11
πŸ”§ fix: Address codex iter 9 β€” pin read_file lookup to primed skill _id
danny-avila Apr 19, 2026
809e622
🧹 fix: Address independent reviewer findings (DRY + types + tests + d…
danny-avila Apr 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion api/server/controllers/agents/openai.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,13 @@ const OpenAIChatCompletionController = async (req, res) => {
tool_resources: primaryConfig.tool_resources,
actionsEnabled: primaryConfig.actionsEnabled,
});
return enrichWithSkillConfigurable(result, req, primaryConfig.accessibleSkillIds);
return enrichWithSkillConfigurable(
result,
req,
primaryConfig.accessibleSkillIds,
undefined,
primaryConfig.manualSkillPrimes?.map((p) => p.name),
);
},
toolEndCallback,
...getSkillToolDeps(),
Expand Down
16 changes: 14 additions & 2 deletions api/server/controllers/agents/responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,13 @@ const createResponse = async (req, res) => {
tool_resources: primaryConfig.tool_resources,
actionsEnabled: primaryConfig.actionsEnabled,
});
return enrichWithSkillConfigurable(result, req, primaryConfig.accessibleSkillIds);
return enrichWithSkillConfigurable(
result,
req,
primaryConfig.accessibleSkillIds,
undefined,
primaryConfig.manualSkillPrimes?.map((p) => p.name),
);
},
toolEndCallback,
...getSkillToolDeps(),
Expand Down Expand Up @@ -676,7 +682,13 @@ const createResponse = async (req, res) => {
tool_resources: primaryConfig.tool_resources,
actionsEnabled: primaryConfig.actionsEnabled,
});
return enrichWithSkillConfigurable(result, req, primaryConfig.accessibleSkillIds);
return enrichWithSkillConfigurable(
result,
req,
primaryConfig.accessibleSkillIds,
undefined,
primaryConfig.manualSkillPrimes?.map((p) => p.name),
);
},
toolEndCallback,
...getSkillToolDeps(),
Expand Down
9 changes: 8 additions & 1 deletion api/server/services/Endpoints/agents/initialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,13 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
});

logger.debug(`[ON_TOOL_EXECUTE] loaded ${result.loadedTools?.length ?? 0} tools`);
return enrichWithSkillConfigurable(result, req, ctx.accessibleSkillIds, codeApiKey);
return enrichWithSkillConfigurable(
result,
req,
ctx.accessibleSkillIds,
codeApiKey,
ctx.manualSkillNames,
);
},
toolEndCallback,
...getSkillToolDeps(),
Expand Down Expand Up @@ -295,6 +301,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
tool_resources: primaryConfig.tool_resources,
actionsEnabled: primaryConfig.actionsEnabled,
accessibleSkillIds: primaryConfig.accessibleSkillIds,
manualSkillNames: primaryConfig.manualSkillPrimes?.map((p) => p.name),
});

const agent_ids = primaryConfig.agent_ids;
Expand Down
10 changes: 9 additions & 1 deletion api/server/services/Endpoints/agents/skillDeps.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,23 @@ function getSkillToolDeps() {
* @param {object} req - The Express request object
* @param {Array} accessibleSkillIds - Pre-computed accessible skill IDs
* @param {string} [preResolvedCodeApiKey] - Pre-resolved code API key (skips redundant lookup)
* @param {string[]} [manualSkillNames] - Skill names manually invoked this turn via the `$` popover
* @returns {Promise<object>} Augmented result with skill configurable
*/
function enrichConfigurable(result, req, accessibleSkillIds, preResolvedCodeApiKey) {
function enrichConfigurable(
result,
req,
accessibleSkillIds,
preResolvedCodeApiKey,
manualSkillNames,
) {
return enrichWithSkillConfigurable(
result,
req,
accessibleSkillIds,
loadAuthValues,
preResolvedCodeApiKey,
manualSkillNames,
);
}

Expand Down
14 changes: 5 additions & 9 deletions client/src/components/Chat/Input/SkillsCommand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { memo, useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { ScrollText } from 'lucide-react';
import { AutoSizer, List } from 'react-virtualized';
import { Spinner, useCombobox } from '@librechat/client';
import { InvocationMode } from 'librechat-data-provider';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import type { TSkillSummary } from 'librechat-data-provider';
import type { MentionOption } from '~/common';
Expand All @@ -22,16 +21,13 @@ const skillIcon = <ScrollText className="icon-md text-cyan-500" />;

/**
* Determines whether a skill should appear in the `$` command popover.
* `manual` and `both` are user-invocable. `auto` is model-only and hidden.
* Skills without an explicit mode (undefined) default to visible for
* backward compatibility until the backend persists `invocationMode`.
* Reads the persisted `userInvocable` field (mirrors the `user-invocable`
* frontmatter). Defaults to visible when the field is absent so older
* skills authored before Phase 6 stay user-invocable without a migration;
* only an explicit `false` hides them.
*/
export function isUserInvocable(skill: TSkillSummary): boolean {
const mode = skill.invocationMode;
if (mode == null || mode === InvocationMode.both) {
return true;
}
return mode === InvocationMode.manual;
return skill.userInvocable !== false;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import React from 'react';
import { act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { InvocationMode } from 'librechat-data-provider';
import type { TSkillSummary } from 'librechat-data-provider';

const CONVO_ID = 'convo-1';
Expand Down Expand Up @@ -412,7 +411,7 @@ describe('filterSkillsForPopover', () => {
const inactive = () => false;
const s1 = makeSkill({ _id: '1', name: 'a' });
const s2 = makeSkill({ _id: '2', name: 'b' });
const s3 = makeSkill({ _id: '3', name: 'c', invocationMode: InvocationMode.auto });
const s3 = makeSkill({ _id: '3', name: 'c', userInvocable: false });

it('passes everything through when agentSkillIds is undefined', () => {
const out = filterSkillsForPopover([s1, s2], { agentSkillIds: undefined, isActive: active });
Expand Down Expand Up @@ -440,7 +439,7 @@ describe('filterSkillsForPopover', () => {
expect(out.map((s) => s._id)).toEqual(['2']);
});

it('excludes auto-only skills via isUserInvocable', () => {
it('excludes skills with userInvocable: false via isUserInvocable', () => {
const out = filterSkillsForPopover([s1, s3], { agentSkillIds: null, isActive: active });
expect(out.map((s) => s._id)).toEqual(['1']);
});
Expand All @@ -456,8 +455,8 @@ describe('filterSkillsForPopover', () => {
agentSkillIds: ['1', '2', '3'],
isActive,
});
/* s1 passes (active, manual-by-default, scoped), s2 drops (inactive),
s3 drops (auto-only invocation mode). */
/* s1 passes (active, user-invocable by default, scoped), s2 drops (inactive),
s3 drops (userInvocable: false). */
expect(out.map((s) => s._id)).toEqual(['1']);
});

Expand Down
Loading
Loading