LibreChat/.github/workflows
Marco Beretta f6ee2ea0ee 📜 feat: Skills UI + Initial E2E CRUD / Sharing (#12580)
* 🎨 feat: Skills UI — Create/Edit/Share/List with Conditional File Tree

First-pass UI on top of the CRUD API scaffolding (#12613). Ships the full
user-facing flow for inline, single-SKILL.md skills and leaves a clean
drop-in for phase-2 multi-file support.

- Create a skill from /skills/new with name (kebab-case, validated),
  description, and SKILL.md body — wired to the real `useCreateSkillMutation`
  and `TCreateSkill` payload.
- List skills in a sidebar (SkillsSidePanel) via `useListSkillsQuery` with
  live search filtering.
- Edit any skill the caller has EDIT permission on — `useUpdateSkillMutation`
  passes `expectedVersion` for optimistic concurrency and surfaces 409
  conflicts as a warning toast + cache refetch.
- Non-blocking `TSkillWarning[]` (e.g. "description too short") are shown
  inline above the form after a successful create/patch.
- Read-only mode when the current user lacks EDIT — the form still renders
  but inputs are marked `readOnly` and the save/reset buttons are hidden.
- Share via ACL using the existing `GenericGrantAccessDialog` — the
  `ShareSkill` button is gated on the SHARE permission.
- Delete with confirmation, driven by `useDeleteSkillMutation({ id })`.
- Conditional file tree: only rendered when `useListSkillFilesQuery`
  returns > 0 files. The tree groups flat `relativePath` strings into a
  nested view (no `react-arborist` dependency) and supports per-file
  deletion via `useDeleteSkillFileMutation`. Upload is intentionally
  deferred — the backend stubs it at 501 in phase 1.

- New routes: `/skills`, `/skills/new`, `/skills/:skillId`.
- Sidebar accordion (`SkillsAccordion` wrapping `SkillsSidePanel`) added
  to `useSideNavLinks` gated on `PermissionTypes.SKILLS` USE.

The initial UI branch (#12580) shipped a lot of exploration code on top of
a now-superseded placeholder backend. Kept as complementary: the `Skills/`
component tree, translation keys, role descriptions, `PublicSharingToggle`
SKILL mapping, `resources.ts` SKILL config, `useCanSharePublic` SKILL
mapping, and `data-provider/roles.ts` `useUpdateSkillPermissionsMutation`.

Deferred out of this first pass:
- Skill favorites (`useSkillFavorites`, `getSkillFavorites` endpoint) —
  the backend route doesn't exist yet; saving for a follow-up.
- AgentConfig `SkillSelectDialog` integration — the UI branch had this
  gated behind `false &&`; rolled back with the config.
- `InvocationMode` / `CategorySelector` / `parseSkillMd` / tree-node
  mutations — not in the Anthropic skill spec and not in the CRUD API.
- `react-arborist` dependency — replaced with a hand-rolled recursive
  tree built from flat `TSkillFile[]`.

- 38 data-schemas skill model tests: pass
- 25 api skill route tests: pass
- 16 user-controller cleanup tests: pass

* 🔐 feat: Default-On Skills in Interface Config and Role Seeder

The skills accordion was registered in the side nav gated on
`PermissionTypes.SKILLS` USE, but no one was actually seeding that
permission on startup, so a fresh install had the USER role with
zero skill permissions and the accordion never rendered.

Fixes three gaps:

1. `interfaceSchema` in data-provider's `config.ts` had no `skills`
   field at all. Added it alongside the existing agents/prompts shape
   (boolean | { use, create, share, public }) and a default of
   `{ use: true, create: true, share: false, public: false }`.

2. `loadDefaultInterface` in data-schemas passed every interface key
   through to the loaded config EXCEPT `skills`. Added the one-line
   passthrough so `appConfig.interfaceConfig.skills` is actually
   populated on boot.

3. `updateInterfacePermissions` in packages/api/src/app/permissions.ts
   seeds role permissions from the interface config on every restart.
   Added:
   - `SKILLS` case to `hasExplicitConfig`
   - `skillsDefaultUse/Create/Share/Public` extraction (mirrors
     prompts/agents)
   - `PermissionTypes.SKILLS` block in `allPermissions` that falls
     through config → roleDefaults → schema default, same pattern as
     AGENTS and PROMPTS
   - `SKILLS` entry in the share-backfill array so that pre-existing
     SKILL role docs missing SHARE/SHARE_PUBLIC get them filled on the
     next restart

Test expectations updated: seven `expectedPermissionsFor(User|Admin)`
blocks in `permissions.spec.ts` now include SKILLS, matching the
role-default values (USER: use+create true, share/public false;
ADMIN: all true).

Result: on a fresh install, a regular USER gets skill USE/CREATE
and the "Skills" accordion shows up in the chat side panel without
any yaml config. Admins can lock it down per role or per tenant via
`interface.skills` in librechat.yaml.

Tests:
- 34 packages/api permissions.spec.ts: pass
- 151 packages/api app tests: pass
- 38 data-schemas skill.spec.ts: pass
- 928 data-provider tests: pass
- 25 api skills.test.js: pass

* ♻️ fix: Resolve Skills UI Review Findings

Addresses the 13 findings from the PR review against the prior commit.

1. **canEdit consistency** — extracted `useSkillPermissions(skill)` as the
   single source of truth for owner/admin/ACL gating. `SkillsView`,
   `SkillForm`, `ShareSkill` all consume it; `SkillFileTree`'s per-file
   delete button now honors admin + EDIT-bit permissions instead of just
   ownership. Unit tests cover owner, admin, editor-ACL, viewer-ACL,
   owner-ACL, loading, and undefined-skill cases.

2. **Disabled submit buttons** — create/edit form submit buttons now set
   native `disabled` (not just `aria-disabled`) during `isLoading`.
   `onSubmit` also guards with an early return when the mutation is still
   in-flight so a duplicate enter-key submit can't create two skills.

3. **Wrong maxLength error message** — description/name `maxLength` rules
   no longer re-use `com_ui_skill_*_required`. Added dedicated
   `com_ui_skill_name_too_long` and `com_ui_skill_description_too_long`
   keys with the literal limit interpolated (`{{0}}`).

4. **Search debouncing** — `SkillsSidePanel` now threads the filter input
   through the existing `useDebounce` hook (250ms) so typing "skills" no
   longer fires six separate list queries.

5. **Frontend test coverage** — added:
   - `tree.test.ts` (9 tests) covering `buildTree` / `nodeKey` edge cases:
     empty input, single root file, multiple roots, nested folders,
     deeply-nested trees, lexicographic sort, empty paths, stable keys
   - `useSkillPermissions.test.ts` (7 tests) covering every precedence
     branch (owner / admin / EDIT / VIEW / owner-ACL / loading / undef)

   Form integration tests proved flaky against react-hook-form's async
   `isValid` with our jest-dom mock setup; deferred to a follow-up PR
   with a proper `@librechat/client` test harness.

6. **Shared `SKILL_NAME_PATTERN`** — promoted the regex plus the four
   length constants (`SKILL_NAME_MAX_LENGTH`,
   `SKILL_DESCRIPTION_MAX_LENGTH`, `SKILL_DESCRIPTION_SHORT_THRESHOLD`,
   `SKILL_DISPLAY_TITLE_MAX_LENGTH`, `SKILL_BODY_MAX_LENGTH`) out of
   `packages/data-schemas/src/methods/skill.ts` and into
   `packages/data-provider/src/types/skills.ts`. The data-schemas
   module now aliases the shared exports so the backend validator and
   the frontend form share one source of truth. Also fixed a latent bug:
   the client regex was stricter than the backend
   (`^[a-z0-9]+(?:-[a-z0-9]+)*$` vs. the real `^[a-z0-9][a-z0-9-]*$`),
   which would have rejected valid names like `foo--bar` client-side.

7. **Removed hardcoded "Claude"** — replaced `com_ui_skill_description_help`
   ("Claude uses this to...") with a new `com_ui_skill_create_subtitle`
   for the form header and `com_ui_skill_description_field_hint`
   ("This is what the model reads to decide...") for the inline hint.
   LibreChat is LLM-agnostic; the old copy misled GPT/Gemini users.

8. **Lifted tree mutation hook** — `useDeleteSkillFileMutation` is now
   instantiated once in `SkillFileTree` (not per `TreeRow`). A
   `TreeContext` provides `onDeleteFile` + `isDeleting` + `canEdit` to
   rows. A 60-node tree used to instantiate 60 mutation hooks; it now
   instantiates one.

9. **List O(n) re-render** — `SkillListItem` no longer reads
   `useParams()` directly. `SkillList` reads the active id once and
   passes `isActive` as a prop, so navigation only re-renders the two
   items whose `isActive` flipped (memo'd), not all N items.

10. **Deduped help text** — the field-level hint and form-level subtitle
    now use different translation keys with distinct copy instead of
    showing the same sentence twice on the same page.

11. **Removed ineffective `useCallback`** — `DeleteSkill.handleDelete`,
    `CreateSkillForm.onSubmit` / `.handleCancel`, `SkillForm.onSubmit`,
    and `SkillFileTree.handleDeleteFile` all wrapped closures around
    React Query `mutation` refs, whose identities change every render.
    Their dep arrays invalidated every render, making the memo a no-op
    with extra overhead. `SkillFileTree` now destructures the stable
    `mutate` function and inlines the arrow inside the memoized
    `contextValue` — one stable reference per deps change.

12. **Import order** — fixed shortest→longest package ordering and
    longest→shortest local ordering across all touched skill files per
    AGENTS.md. `react` always first where imported.

13. **Memoization principle** — documented the rule with inline comments:
    `memo` on components that appear in repeated contexts (`TreeRow`,
    `SkillListItem`) or as children of frequently-re-rendering parents
    (`ShareSkill` / `DeleteSkill` under `SkillForm`'s per-keystroke
    form-state updates). Removed `memo` from `SkillFileTree` since its
    parent `SkillDetailPanel` only re-renders on query-data changes.

- 38 data-schemas skill.spec.ts
- 34 packages/api permissions.spec.ts
- 25 api skills.test.js
- 16 client unit tests (9 buildTree + 7 useSkillPermissions)
- All type-checks + eslint clean on touched files

* 🧹 fix: Skills Duplication, Input Styling, Remove LLM-specific Copy

Three UI fixes from an in-chat review pass:

1. **Sidebar duplication** — `SkillsView` was rendering its own
   `SkillsSidePanel` aside alongside the chat side panel's
   `SkillsAccordion`, so on `/skills` the user saw the skill list
   twice. Fixed by mirroring the `InlinePromptsView` pattern: the
   route content is now just the detail / create panel and the
   chat side panel is the sole list. Added `/skills → /skills/new`
   redirect and a `/skills/new` literal route so `useParams().skillId`
   is `undefined` for "new" (matches prompts).

2. **Name Input styling** — the big floating-label pattern used by
   prompts/agents for the primary name field was replaced with a
   conventional `<Label>` + `<Input>` above it, diverging from the
   rest of the app. Restored the prompts-style `text-2xl` input with
   the peer-focus animated label on both `CreateSkillForm` and
   `SkillForm`. Kept the conventional pattern for description and
   body since they're textareas.

3. **Remove LLM-specific copy from skill translations** — dropped
   `com_ui_skill_description_help` ("Claude uses this to...") and
   the transitional "This is what the model reads..." phrasing.
   Field hint is now a neutral "Be specific about when this skill
   should apply." and the create-page subtitle is a neutral "Author
   a new skill your agents can invoke." LibreChat is LLM-agnostic;
   baking product names into user-facing copy is wrong outside the
   `com_endpoint_anthropic_*` keys where the setting actually only
   applies to Claude models.

Side-effect: the `SkillDetailView` wrapper in `SkillsView` now only
renders the file-tree aside when the skill has > 0 files — same
conditional-tree behavior as before, just scoped to this route
instead of also trying to also render a list sidebar.

- 16 client skill tests still pass
- Type-check + eslint clean on touched files

* 🎁 feat: Restore Skills UI from PR #12580

Brings back everything the original UI PR (#12580, commit da039917c)
shipped that my earlier rebase dropped. Verbatim restores where possible;
adapts the new hooks/types where the backend contract has shifted.

**Scoped-out / gated-off (now restored as inert UI scaffolding):**
- `hooks/useSkillFavorites.ts` + `utils/favoritesError.ts` + the
  `useGetSkillFavoritesQuery` / `useUpdateSkillFavoritesMutation` additions
  in `data-provider/Favorites.ts`. The backend route doesn't exist yet —
  the data-service functions resolve with empty arrays so the Star UI is a
  visual-only no-op until phase 2.
- `dialogs/SkillSelectDialog.tsx` + the "Add Skills" section in
  `SidePanel/Agents/AgentConfig.tsx` (still gated behind the original
  `false &&`) + `skills?: string[]` on `AgentForm` / `Agent` /
  `AgentCreateParams` / `AgentUpdateParams` + the `skills: []` entry in
  `defaultAgentFormValues`.
- `TUserFavorite.skillId` reserved on the shared favorites type.

**Concept-is-gone / deleted-types (restored as UI-only types + stubs):**
- `InvocationMode` enum and `TSkillNode`, `TSkillTreeResponse`,
  `TCreateSkillNodeRequest`, `TUpdateSkillNodeRequest` types in
  `packages/data-provider/src/types.ts`. UI-facing only; the backend flat
  `TSkillFile[]` contract is unchanged.
- `TSkill.invocationMode?: InvocationMode` as an optional field. Forms
  read/write it in local state and deliberately drop it from the PATCH
  payload until the backend column lands.
- `tree/SkillFileTree.tsx` (`react-arborist`-based), `SkillTreeNode.tsx`,
  `TreeToolbar.tsx`, `SkillFileEditor.tsx`, `SkillFilePreview.tsx` — full
  filesystem-style browser UI restored verbatim.
- `data-provider/Skills/tree-queries.ts` + `tree-mutations.ts` hooks
  (`useGetSkillTreeQuery`, `useCreateSkillNodeMutation`, etc.). The
  `data-service` stubs them: `getSkillTree` returns `{ nodes: [] }`,
  `createSkillNode` / `updateSkillNode` / `updateSkillNodeContent` return
  synthetic node shapes, `deleteSkillNode` resolves void. Hooks compile
  and run; tree is empty until phase 2 wires a real backend.
- `MutationKeys.createSkillNode` / `updateSkillNode` / `deleteSkillNode` /
  `updateSkillNodeContent` + `CreateSkillNodeBody` /
  `UpdateSkillNodeVariables` / `DeleteSkillNodeBody` /
  `UpdateSkillNodeContentVariables` types.
- `QueryKeys.skillTree` / `skillNodeContent` / `skillFavorites` /
  `favorites` and the `skillTree()` endpoint helper.

**Scope-simplified (restored with minimal adaptation):**
- `display/SkillDetailHeader.tsx` + `display/SkillDetail.tsx`. Header now
  falls back to `InvocationMode.auto` when `skill.invocationMode` is
  undefined.
- `forms/SkillContentEditor.tsx` — click-to-edit markdown preview toggle
  for the SKILL.md body field. Wired into both `CreateSkillForm` and
  `SkillForm` replacing the plain `<TextareaAutosize>`.
  (Needed `@ts-ignore` on `remarkPlugins` / `rehypePlugins` for the same
  `PluggableList` vs `Pluggable[]` shape drift `MarkdownLite.tsx` already
  works around.)
- `forms/InvocationModePicker.tsx` + `forms/CategorySelector.tsx` — the
  auto/manual/both dropdown and the skill category selector. Wired into
  both forms inside a `FormProvider` so the Controller-based widgets can
  read `useFormContext`. `category` flows to the PATCH / POST payload as
  before; `invocationMode` is UI-only per the type note above.
- `buttons/CreateSkillMenu.tsx` + `utils/parseSkillMd.ts` — dropdown with
  AI / Manual / Upload SKILL.md entries + the YAML frontmatter parser for
  the upload path. `CreateSkillForm.defaultValues` now accepts the parsed
  shape, so the upload → redirect → pre-populated form flow works again.
- `buttons/AdminSettings.tsx` — admin permissions dialog. Uses the
  existing `useUpdateSkillPermissionsMutation` which was already wired.
- `sidebar/FilterSkills.tsx` — restored filter + AdminSettings +
  CreateSkillMenu wrapper. `SkillsSidePanel.tsx` is back to the original
  `FilterSkills`-based layout.
- `lists/SkillList.tsx` + `lists/SkillListItem.tsx` — restored verbatim.
- `layouts/SkillsView.tsx` — restored the full tree + file editor + file
  preview layout. The chat side panel keeps its own accordion list; this
  view is the inline detail experience.
- `hooks/Generic/useUnsavedChangesPrompt.ts` — route-leave guard hook.

- `useGetSkillByIdQuery` is aliased to `useGetSkillQuery` so restored
  components (`SkillsView`, `SkillForm`) that import the old name resolve
  to the new hook.
- `SkillSelectDialog` + `AgentConfig` coerce `skillsData?.skills` instead
  of `.data` (list response shape drift from the CRUD PR).
- `CreateSkillForm` / `SkillForm` wrap their JSX in `FormProvider` so the
  restored `CategorySelector` and `SkillContentEditor` components —
  which read `useFormContext` — work inside the existing forms without
  another refactor.
- `CreateSkillForm.defaultValues` prop accepts `Partial<Values> &
  { invocationMode?: unknown }` so the upload flow's
  `{ name, description, invocationMode }` shape passes through cleanly.
- `SkillsView` route map gains `/skills/:skillId/edit` and
  `/skills/:skillId/file/:nodeId` so the tree-navigation URLs the original
  view produces actually resolve.
- `client/package.json` gains `react-arborist@^3.4.3`.
- ~60 translation keys the restored files reference — invocation labels,
  edit/create page titles, file editor chrome, tree toolbar tooltips,
  favorites, admin allow-settings, unknown-file-type, sr_public_skill,
  delete/rename _var variants — all added to `en/translation.json`.

- Prompts-style floating-label name input — kept from my earlier commit
  so it matches the rest of the app (user reviewed and approved that
  styling). Hidden skill-body textarea is replaced by `SkillContentEditor`
  in both forms.

- 38 data-schemas skill.spec.ts
- 34 packages/api permissions.spec.ts
- 25 api skills.test.js
- 7 client useSkillPermissions.test.ts
- Type-check: pre-existing error count (188) dropped to 120 because my
  restorations fixed some previously-broken field types.

* chore: Update package-lock.json to include react-arborist and memoize-one

* feat: Add support for react-arborist in Vite configuration

This update introduces a new condition in the Vite configuration to handle the 'react-arborist' package, ensuring it is properly recognized during the build process. This change enhances compatibility with the recently added 'react-arborist' dependency in the project.

* 🩹 fix: Hide InvocationMode, Fix SkillContentEditor Click-to-Edit

1. Hide InvocationModePicker from both CreateSkillForm and SkillForm.
   Component stays on disk for when the backend lands the column.

2. Fix "Click to edit" doing nothing on SkillContentEditor. The
   `onBlur={() => setIsEditing(false)}` on the TextareaAutosize was
   racing with `autoFocus` — React renders the textarea, autoFocus
   fires, then a layout/reconciliation blur fires immediately,
   bouncing back to preview mode before the user can interact.
   Removed onBlur; users toggle via the header button or Escape key.

* 🎨 feat: Reader-First Skills UI — Match Claude.ai Layout

Reworks the Skills UI from form-first to reader-first, matching
Claude.ai's skill detail pattern.

**Default view is now read-only.** Clicking a skill in the sidebar
navigates to `/skills/:id` which renders `SkillDetail` — a clean
content view with:
- Skill name as the primary heading
- Metadata row: "Added by" + "Last updated" (formatted date)
- Description block
- Rendered SKILL.md body in a bordered card with a source/rendered
  toggle (eye + code icons, matching Claude.ai's segmented control)

No form fields, no save/cancel buttons. The user reads the skill
first and takes action deliberately.

**Create is now a dialog.** The `/skills/new` route is gone.
`CreateSkillMenu` (the + dropdown in the sidebar) now opens
`CreateSkillDialog` — a minimal modal with name, description, and
instructions fields. Upload-from-file still works: parse → populate
dialog → create. Matches Claude.ai's "Write skill instructions"
modal.

**Edit is behind an action.** The detail view shows an "Edit" button
(permission-gated) that navigates to `/skills/:id/edit`, rendering
the existing `SkillForm`. The edit route is preserved for direct
linking.

**Navigation goes to detail, not edit.** `SkillListItem` now
navigates to `/skills/:id` (detail) instead of `/skills/:id/edit`.

- `display/SkillMarkdownRenderer.tsx` — shared ReactMarkdown
  component extracted from `SkillContentEditor`. Same remark/rehype
  plugins, no form dependency.
- `display/SkillDetail.tsx` — the reader-first view (replaces the
  old thin wrapper).
- `dialogs/CreateSkillDialog.tsx` — OGDialog modal for skill
  creation.

- `layouts/SkillsView.tsx` — gutted and rebuilt. Three states:
  no-skill (empty state), skillId (SkillDetail), skillId+edit
  (SkillForm). Removed full-page CreateSkillForm, removed TreeView.
- `buttons/CreateSkillMenu.tsx` — opens dialog instead of navigating
  to `/skills/new`. Upload flow: parse → set dialog defaults → open.
- `lists/SkillListItem.tsx` — navigate to detail, not edit.
- `routes/index.tsx` — removed `/skills/new` and file/nodeId routes;
  `/skills` renders SkillsView directly (empty state).
- `display/index.ts`, `dialogs/index.ts` — added new exports.
- `locales/en/translation.json` — added ~10 new keys for metadata,
  toggle labels, dialog title, empty state.

* 🩹 fix: SkillContentEditor click-to-edit z-index — button was z-0 behind rendered content

* 🩹 fix: Align Edit button size with Share/Delete (size-9)

* 🎨 feat: Claude.ai-Style Skill List Panel

Rewrites the skills sidebar to match Claude.ai's panel layout:

- Header: "Skills" title + search icon (toggles input) + add icon
  (opens CreateSkillDialog directly, no dropdown menu)
- Collapsible "Skills" section with chevron toggle
- Skill items: 24px icon badge (rounded square with ScrollText icon)
  + name only. No description text in the list — that lives in the
  detail view. Active item gets highlighted bg + bold font.
- Removed AdminSettings button from sidebar header — admin config
  is accessible via the admin dashboard, not cluttering every user's
  skill list.
- Removed FilterSkills wrapper (was Filter + AdminSettings +
  CreateSkillMenu). The search + create are now inline in the panel
  header.

Files changed:
- sidebar/SkillsSidePanel.tsx — full rewrite
- sidebar/SkillsAccordion.tsx — simplified wrapper
- lists/SkillList.tsx — collapsible section, no description
- lists/SkillListItem.tsx — icon badge + name, memo'd

* 🎨 fix: Align Skills UI Styling with Prompts Patterns

Style alignment pass based on direct comparison with claude.ai and
the existing prompts preview dialog.

SkillsSidePanel search now replaces the title in the header row when
toggled (search icon + input + X close), matching Claude.ai's pattern.
Previously it pushed a separate input below the header, wasting
vertical space. Close button clears the search term.

Replaced `text-text-tertiary` with `text-text-secondary` across
SkillDetail, SkillList, SkillForm, CreateSkillForm, CreateSkillDialog,
SkillContentEditor. Tertiary was too dark / low contrast.

SkillList section chevron label now reads "Personal skills" (matching
Claude.ai) via the existing `com_ui_my_skills` key, instead of the
generic "Skills" which duplicated the header.

Aligned with `PromptDetailHeader` styling:
- 48px round icon (ScrollText in bg-surface-secondary circle)
- Name + public badge in the icon row
- Metadata below the icon: User icon + author, Calendar icon + date
  (text-xs text-text-secondary with gap-3, matching prompts exactly)
- Description uses the same label-above-text pattern as prompts
- Content card uses `bg-transparent` border (not bg-surface-primary-alt)
- Toggle buttons use size-5 icons and text-text-secondary for inactive

Changed from `max-w-lg p-0` to `max-w-5xl` with the same max-height
and padding pattern as the prompts PreviewPrompt dialog:
`max-h-[80vh] p-1 sm:p-2 gap-3 sm:gap-4`. Close button now renders
via default OGDialogContent behavior (removed showCloseButton=false).

* 🩹 fix: SkillDetail fills parent height, tighter spacing (px-6 pb-6 gap-2)

* 🩹 fix: Align Skills panel header padding (px-4) with list content below

* 🩹 fix: Reduce Skills header top padding (pt-2) to align with sidebar icon strip

* 🩹 fix: Tighten Skills header (py-2) and detail top (py-2) to align with sidebar icons and match edit view

* 🩹 fix: Offset SidePanel Nav pt-2 with -mt-2 on SkillsAccordion so Skills header aligns with icon strip

* 🛠️ fix: Increase Node memory limit for production build in package.json

* 🩹 fix: Remove top padding from SkillDetail header row (py-2 → pb-2)

* 🏗️ refactor: Move pt-2 from SidePanel/Nav wrapper to each panel

Removed the global `pt-2` from `SidePanel/Nav.tsx` and pushed it
into each panel's own top-level wrapper. This lets each panel own
its vertical alignment independently — Skills can sit flush at the
top to align with the sidebar icon strip, while other panels keep
their original spacing.

Panels updated with `pt-2`:
- PromptsAccordion (via className on PromptSidePanel)
- BookmarkPanel
- FilesPanel
- MemoryPanel
- MCPBuilderPanel
- AgentPanel (form wrapper)
- AssistantPanel (form wrapper)
- ParametersPanel (already had pt-2)

SkillsAccordion: removed the -mt-2 hack, now naturally flush.

* 🧹 fix: Align CreateSkillDialog field styling + remove 19 unused i18n keys

Dialog fields: all three inputs now use consistent `rounded-xl
border-border-medium px-3 py-2 text-sm` styling. Replaced the
`<Input>` component with a plain `<input>` to avoid the component's
built-in `rounded-lg border-border-light` overriding the dialog's
border style. Labels use `font-medium` for consistency.

Removed 19 unused translation keys from translation.json:
com_ui_skill_body, com_ui_skill_body_placeholder,
com_ui_skill_create_subtitle, com_ui_skill_file_delete_confirm,
com_ui_skill_file_delete_error, com_ui_skill_file_deleted,
com_ui_skill_files_empty, com_ui_skill_files_multi_hint,
com_ui_skill_list, com_ui_skill_load_error,
com_ui_skill_resize_file_tree, com_ui_skill_select_file,
com_ui_skill_select_file_desc, com_ui_skills_load_error,
com_ui_add_first_skill, com_ui_create_skill_page,
com_ui_edit_skill_page, com_ui_save_skill, com_ui_no_skills_title

* 🎁 feat: Upload Skill Dialog + Simplified Create Menu

New `UploadSkillDialog` matching Claude.ai's upload modal:
- Dashed drop zone with drag-and-drop support
- Accepts .md, .zip, .skill files
- Phase 1: processes .md files (parses YAML frontmatter → creates
  skill with body as the full file content)
- Shows file requirements below the drop zone
- On success: navigates to the new skill's detail view

`CreateSkillMenu` now has two flat options (no sub-menu):
- "Write skill instructions" → opens `CreateSkillDialog`
- "Upload a skill" → opens `UploadSkillDialog`

Removed the disabled "Create with AI" option and the old file input
hidden-element approach. The sidebar `+` button now renders
`CreateSkillMenu` directly instead of a standalone create dialog.

- Removed 5 unused i18n keys (com_ui_skill_added_by,
  com_ui_skill_last_updated, com_ui_skills_add_first,
  com_ui_skills_filter_placeholder, com_ui_skills_new)
- Tightened metadata gap in SkillDetail (mt-1 → mt-0.5)
- Added 7 new upload-related i18n keys

* 🔒 feat: Zip/Skill File Upload Support with Safety Limits

Rewrites UploadSkillDialog to properly handle all three accepted
file types:

- `.md` — reads as text, parses YAML frontmatter, creates skill
- `.zip` / `.skill` — reads as ArrayBuffer, extracts with JSZip,
  finds SKILL.md (at root or one level deep), parses its content,
  creates skill. Shows spinner during processing.

Security guards against zip bombs:
- MAX_ZIP_SIZE: 50MB compressed file limit
- MAX_ENTRIES: 500 file limit inside the archive
- Path traversal rejection: skips entries with `..` or leading `/`
- SKILL.md search limited to depth ≤ 2 segments

Added `jszip@^3.10.1` to client dependencies (already in the
monorepo's node_modules from backend usage).

The name is inferred from the zip filename if SKILL.md frontmatter
doesn't have one (e.g. `skills-autofix.zip` → `skills-autofix`).

* 🚀 feat: Backend Skill Import + Live File Upload Endpoints

New endpoint that accepts a single multipart file (.md, .zip, .skill)
and creates a skill with all its files in one request:

- **.md**: parse YAML frontmatter → create skill with body
- **.zip / .skill**: extract with JSZip, find SKILL.md (root or one
  level deep), create skill from its content, then persist every
  additional file via `upsertSkillFile` + local file storage strategy.
  Returns the created skill + an `_importSummary` with per-file
  results.

Security:
- 50MB compressed file size limit (multer)
- 500 max entries in archive
- 10MB per individual file
- Path traversal rejection (no `..`, no absolute, validated charset)
- File type filter: only .md/.zip/.skill accepted
- Rate limited via existing `fileUploadIpLimiter` + `fileUploadUserLimiter`

Handler lives in `packages/api/src/skills/import.ts` with injectable
deps (`createSkill`, `upsertSkillFile`, `saveBuffer`) for testability.

Replaced the 501 stub with a real handler:
- Accepts multipart FormData with `file` + `relativePath`
- Saves file via local storage strategy
- Calls `upsertSkillFile` to persist the SkillFile record
- Returns the upserted document
- Rate limited, ACL-gated (EDIT permission required)
- 10MB per file limit

`UploadSkillDialog` now sends the file to `/api/skills/import` via
`dataService.importSkill(formData)` — no more client-side JSZip.
Removed `jszip` from client dependencies (only backend needs it).

Added `importSkill()` in data-service + `importSkill()` endpoint
builder in api-endpoints.

Updated the file upload test from expecting 501 stub to expecting
400 "no file provided" (live validation). All 25 skill route tests
pass.

* 🔒 fix: Complete Import Handler — Validation, Ownership, Error Surfacing

Fixes several gaps in the skill import flow:

1. **Skill validation now runs and surfaces properly.** The import
   handler calls the real `createSkill(CreateSkillInput)` which runs
   `validateSkillName`, `validateSkillDescription`, `validateSkillBody`.
   Validation errors (SKILL_VALIDATION_FAILED) are caught and returned
   as 400 with the issue messages. Duplicate-key errors return 409.
   Previously all errors were swallowed into a generic 500.

2. **`authorName` is now populated.** The `CreateSkillInput` requires
   `authorName` which was missing — resolved from `req.user.name ??
   req.user.username ?? 'Unknown'`, matching the existing create handler.

3. **SKILL_OWNER permission is granted after import.** Calls
   `grantPermission` with `AccessRoleIds.SKILL_OWNER` so the uploader
   can edit/delete/share the imported skill. This was entirely missing —
   imported skills would have been ownerless.

4. **`tenantId` propagated.** Both the skill and each SkillFile record
   receive `req.user.tenantId` for multi-tenant deployments.

5. **SkillFile records are created in the DB.** Each non-SKILL.md file
   in the zip is saved to file storage via `saveBuffer` and recorded
   via `upsertSkillFile`, which validates the relativePath, infers the
   category from the path prefix, and atomically bumps the skill's
   `fileCount` and `version`.

Import deps now include `grantPermission` from PermissionService,
injected in `api/server/routes/skills.js`.

* 🐛 fix: Import grant uses accessRoleId (not roleId) — fixes skill not appearing in list

* 🎨 fix: Cache invalidation, file tree, frontmatter rendering

Three fixes for the skill detail view:

1. **Cache invalidation after import.** UploadSkillDialog now calls
   `queryClient.invalidateQueries([QueryKeys.skills])` after a
   successful import so the sidebar list picks up the new skill
   without requiring a page refresh.

2. **File tree in detail view.** When a skill has `fileCount > 0`,
   the detail view now queries `useListSkillFilesQuery` and renders
   a file list below the body card — SKILL.md first, then folders
   and root files. Icons: Folder for directories, FileText for files.

3. **Frontmatter stripped and rendered as metadata.** YAML frontmatter
   (`---\nversion: 0.1.0\ntriggers: ...\n---`) is now parsed out of
   the body before markdown rendering. The `name` and `description`
   fields are skipped (already shown in the header). Remaining fields
   (version, triggers, dependencies, etc.) are displayed in a
   Claude.ai–style grid: label on the left, value on the right,
   above the rendered markdown content. Source view still shows the
   full raw body including frontmatter.

* 🩹 fix: Always fetch skill files — fileCount may be stale in cached skill object

* 🌳 feat: Inline File Tree in Sidebar Skill List

Moves the file tree from the bottom of SkillDetail into the sidebar
list, matching Claude.ai's pattern:

- Multi-file skills show a chevron toggle on the right side of the
  skill list item
- Clicking the chevron expands an inline file tree below the skill
  name: SKILL.md first, then folders (with folder icon + right
  chevron) and root files
- File list is fetched lazily (only when expanded) via
  useListSkillFilesQuery
- Clicking a file navigates to the skill detail view
- Files section removed from SkillDetail — the sidebar is now the
  sole file tree location, keeping the detail panel clean

SkillDetail cleaned up: removed groupFiles helper, file-related
state, useListSkillFilesQuery import, FileText/Folder icon imports.

* 🌲 feat: Virtualized inline file tree with react-vtree

Replace hand-rolled recursive FolderRow/FileRow buttons with a proper
virtualized FixedSizeTree from react-vtree for the sidebar skill list.
Dynamic height tracks open folders; capped at 350px with smooth
expand/collapse transitions.

* chore: Remove no longer used SkillFileTree and SkillTreeNode components

* chore: Update Vite config to replace 'react-arborist' with 'react-vtree' for module resolution

* feat: Skill file content viewing with lazy DB caching

- Add `skills` field to `fileStrategiesSchema` so operators can
  configure a dedicated storage backend for skill files. Falls back
  by type (image/document) when unset.
- Fix hardcoded `FileSources.local` in skill save/import — now uses
  the resolved strategy via `getFileStrategy(req.config, { context })`.
- Replace 501 download stub with real handler that streams from any
  storage backend and returns JSON `{ content, mimeType, isBinary }`.
- Binary detection (null-byte + non-printable ratio on first 8 KB)
  flags files on first read so they're never re-fetched.
- Text content ≤ 512 KB is cached in the SkillFile MongoDB document;
  subsequent reads skip storage entirely.
- Clicking a skill row now expands inline files (not just chevron).
- Clicking a file navigates to `?file=<path>` and renders content
  in a new SkillFileViewer (markdown, code, images, binary placeholder).

* chore: Remove react-window and its type definitions from package.json and package-lock.json

- Deleted `react-window` and `@types/react-window` dependencies from both `package.json` and `package-lock.json` to streamline the project and reduce unnecessary bloat.

* fix: Build errors — remove endpoints import, fix Uint8Array cast

- Replace `import { endpoints }` (not public) with inline URL in
  SkillFileViewer
- Remove `as Uint8Array` cast in stream chunk handling
- Extend getSkillFileByPath return type with content/isBinary to
  decouple from data-schemas build artifact resolution

* chore: Remove 8 unused i18next keys

com_ui_create_skill_ai, com_ui_create_skill_manual,
com_ui_delete_folder_confirm_var, com_ui_delete_skill,
com_ui_delete_skill_confirm_var, com_ui_delete_var,
com_ui_rename_var, com_ui_skill_files

* fix: Add configMiddleware to skills router, handle SKILL.md in viewer

- Add configMiddleware to skills router so req.config is populated
  when getLocalFileStream (or any strategy) reads file paths.
- Handle SKILL.md in download handler — serves skill.body directly
  from the Skill document instead of looking for a SkillFile record.
- Clicking SKILL.md in sidebar tree now opens the file viewer
  (matching Claude.ai behavior: file view vs default detail view).

* ci: Run unit tests on PRs to any branch

Remove the branches filter from both test workflows so contributor
PRs targeting feature branches (not just main/dev) get CI coverage.
Path filters are kept so tests only run when relevant files change.

* fix: Update skills route tests for download handler changes

- Mock configMiddleware (sets req.config for file storage access)
- Mock getStrategyFunctions and getFileStrategy (storage strategy deps)
- Replace 501 stub test with SKILL.md content test + 404 test

* fix: Auto-expand files, frontmatter parsing, select-none, prefetch

- Auto-expand file tree when navigating directly to a skill URL
- Prefetch files for the active skill (eliminates first-expand lag)
- Fix frontmatter parser to handle multi-line YAML list values
  (triggers field was missing because it uses list syntax)
- SkillFileViewer now parses frontmatter for .md files — shows
  structured grid + rendered body (matching SkillDetail's display)
  with source/rendered toggle
- Add select-none to all sidebar skill and file tree buttons

* refactor: Derive expanded state from isActive instead of useEffect

Replace useEffect sync with deterministic derivation:
expanded = hasFiles && (isActive || !collapsed)

Active skill is always open. collapsed is a manual toggle that
only takes effect on non-active items.

* fix: Remove empty space above body card — overlay view toggle

Move the rendered/source toggle from a dedicated row (40px of empty
space) to an absolute-positioned overlay in the card's top-right
corner, matching Claude.ai's layout.

* fix: Remove header bars from content editors — overlay action buttons

Collapse the full-width header bars ("Skill Content", "Text") in
SkillContentEditor, PromptTextCard, and PromptEditor. Action buttons
(edit/save toggle, copy, variables) are now absolute-positioned in
the card's top-right corner, reclaiming ~46px of vertical space.

* fix: Spinner visibility in file viewer — use text-text-secondary

* fix: Address review findings — security, correctness, code quality

Codex P1: Use $unset instead of undefined to clear cached content
and isBinary fields on file re-upload (Mongoose strips undefined).

Codex P2: Match skill-file validation errors by error.code instead
of error.message substring.

F1: Zip bomb defense — track cumulative decompressed bytes (500 MB
cap), check declared uncompressed size before buffering each entry.

F2: Remove misleading "atomically" from import handler JSDoc.

F3: Static import for isBinaryBuffer instead of dynamic import().

F4: Replace console.error with logger in upload handler.

F6: Add multer error handler middleware to skills router.

F7: Move React import to top of SkillDetail.tsx.

F9: Fix variable shadowing (trimmed → item) in parseFrontmatter.

F11: Replace JSON.parse(JSON.stringify()) with toJSON() for
Mongoose document serialization.

F12: Remove dead dynamic import('fs') fallback (memoryStorage
always provides file.buffer).

F13: Hoist MIME_MAP to module scope to avoid per-call allocation.

F16: Share single multer.memoryStorage() instance.

* fix: Follow-up review — close zip bomb gap, fix error handler

F1: Add post-decompression cumulative byte check with break (the
pre-decompression check relies on undocumented JSZip internals
that may be absent; this closes the gap unconditionally).

F2+F3: Multer error handler now forwards non-multer errors via
next(err) instead of swallowing them. Also catches file filter
rejections (plain Error, not MulterError) by message prefix.

F4: Move isBinaryBuffer import to local imports section per
CLAUDE.md import order rules.

F5: Simplify dead toJSON branch — createSkill returns a POJO.

* nit: Link filter error message to handler prefix check

* feat: Accordion expansion + active file highlight in sidebar

- Only one skill's file tree can be expanded at a time (accordion).
  Expansion state lifted from SkillListItem to SkillList.
- Selected file gets bg-surface-active highlight in the tree.
  Skill row uses subtle style (no background) when a file is active,
  matching Claude.ai's pattern where the file — not the skill —
  carries the selection state.

* style: Adjust margin for file tree in SkillListItem component

- Reduced left margin from 10 to 5 for improved layout consistency in the file tree display.

* fix: TS error on FileTreeNode, nested ternary, chevron collapse

- Make style prop optional to match react-vtree's NodeComponentProps
- Flatten nested ternary for skill row active styles
- Skill row click expands (but doesn't collapse) files + navigates
- Chevron click explicitly toggles collapse (matching Claude.ai
  where clicking the chevron is how you collapse files)

* fix: Upload basePath, reject SKILL.md uploads, add skills permission route

- Pass basePath: 'uploads' in per-file upload handler (was defaulting
  to 'images' path, inconsistent with the import flow).
- Reject uploads targeting SKILL.md (reserved path — download handler
  special-cases it to return skill.body, making an uploaded file
  unreachable via the API).
- Add skills entry to roles router permissionConfigs so PUT
  /api/roles/:roleName/skills actually reaches a handler instead
  of returning 404.

* feat: Expand content area, move controls to header, reduce padding

Default detail view:
- Remove rounded-xl bordered card wrapper — content flows directly
  into the article, capitalizing on full screen width
- Move eye/code toggle inline with the divider row
- Reduce px-6/pb-6 to px-4/pb-4

File viewer:
- Move eye/code toggle from card overlay to the header bar
- Add copy-to-clipboard button for text files in the header bar
- Remove rounded-xl bordered card wrapper for markdown content
- Remove bordered pre wrapper for non-markdown text
- Reduce px-6/py-4 to px-4/py-3

Both views maximize content space over decorative chrome.

* fix: Stable header height, restore some padding

- Fix layout shift in file viewer header: use fixed h-10 so the
  bar height stays constant whether the eye/code toggle renders
  (markdown) or not (plain text).
- Bump content padding from px-4/py-3 back to px-5/py-4 in both
  views — the previous reduction was too aggressive.

* fix: Grant rollback, path validation, error format, dead code cleanup

F2: grantOwnership now rolls back (compensating delete) on failure,
matching the create handler. Both markdown and zip import paths
check the result and return 500 on grant failure.

F4: Upload handler validates relativePath with regex + traversal
check before calling downstream upsertSkillFile.

F5: Document JSZip _data.uncompressedSize as best-effort; the
post-decompression cumulative check is the real safety net.

F10: Standardize all upload handler error responses to { error }
(was { message }, inconsistent with handlers.ts).

F13: Single-pass fileResults accumulation in import handler.

F1-5: Remove dead uploadFileStubHandler (no route references it).

Codex P2: Fix delete nav from /skills/new to /skills.

F12: Use cn() in UploadSkillDialog instead of template literals.

* perf: Stream-first binary detection + O(1) public skill check

F1: Download handler now reads only the first 8 KB for binary
detection. If binary, the stream is destroyed immediately without
buffering the remaining file. Text files continue reading for
caching. Eliminates buffering up to 10 MB per request for binary
files under concurrent load.

F7: Single-skill GET and PATCH now use hasPublicPermission (O(1)
ACL lookup) instead of getPublicSkillIdSet (queries ALL public
skill IDs). The list handler still uses the Set approach since it
serializes multiple skills. serializeSkill/serializeSkillSummary
now accept boolean | Set for flexibility.

* fix: Update test to match { error } response format

* fix: Critical stream truncation bug, grantedBy, error format

NF-1 (CRITICAL): Rewrite binary detection to single for-await loop.
Breaking out of for-await-of destroys the stream via iterator.return(),
so the previous two-loop approach silently truncated text files > 8KB.
Now: one loop collects chunks, checks binary after 8KB accumulated,
and either destroys+returns (binary) or continues reading (text).

NF-2: Add grantedBy to import handler's grantPermission call and
interface (was missing, inconsistent with create handler).

NF-3: Standardize all import handler error responses from { message }
to { error }, matching handlers.ts convention. Update client's
UploadSkillDialog to read response.data.error accordingly.

* fix: Prefer specific validation message over generic error field

* fix: YAML quote stripping, saveBuffer null guard, dot segment rejection

- Strip surrounding YAML quotes from frontmatter values so
  name: "my-skill" parses as my-skill (not "my-skill" with quotes
  that fails the name validator).
- Guard resolveSkillStorage against backends with saveBuffer: null
  (e.g. OpenAI/vector strategies) — throws a descriptive error
  caught by the handler's try/catch instead of a TypeError.
- Tighten upload path validation to reject . segments (e.g.
  docs/./a.md) matching the model-layer validator, preventing
  storage writes for paths the DB will reject.

* fix: Orphan cleanup, stream errors, malformed zip, cache latency

F1: Upload handler now deletes the stored blob if the subsequent
DB upsert fails, preventing orphaned files on disk/cloud.

F2: Multer error handler returns { error } (was { message }).

F3: Wrap JSZip.loadAsync in try/catch — malformed zip returns 400
instead of falling through to 500.

F4: Raw download stream gets an error handler — logs the error and
destroys the response if headers were already sent.

F8: Strip leading hyphens from inferred skill name so filenames
like _my-skill.zip don't produce -my-skill (invalid name pattern).

F9: Fire-and-forget all updateSkillFileContent cache writes so the
response is sent immediately. Cache failures are logged but don't
block or fail the read.

* fix: Import orphan cleanup + Content-Disposition sanitization

Finding A: Add deleteFile dep to ImportSkillDeps. The per-file loop
in handleZip now cleans up stored blobs when upsertSkillFile fails,
closing the second half of the F1 orphan fix (upload handler was
already fixed).

Finding B: Sanitize filename in Content-Disposition header for raw
downloads — strip quotes, backslashes, and newlines to prevent
header injection from user-uploaded filenames.

* security: Prevent stored XSS via raw file downloads

Non-image files served via ?raw=true now use Content-Disposition:
attachment (force download) instead of inline. An uploaded .html or
.svg file served inline from the LibreChat origin could execute
scripts with access to the user's session — this closes that vector.

Images stay inline (needed for <img> rendering in SkillFileViewer).
X-Content-Type-Options: nosniff added to prevent MIME sniffing.

* security: Block SVG XSS — allowlist safe raster MIME types for inline

SVG (image/svg+xml) passed the startsWith('image/') check and was
served inline, but SVG is a scriptable format — embedded <script>
tags execute in the LibreChat origin. Replace the prefix match with
a Set of safe raster-only MIME types (png, jpeg, gif, webp, avif,
bmp). SVGs and any future scriptable image/* subtypes now get
Content-Disposition: attachment (forced download).

* fix: Cap JSON text response at 1MB, consistent md name inference

F3: Text files > 1MB now return { isBinary: false } with no content
field, forcing the client to use ?raw=true for download. Prevents
buffering 10MB files into heap for JSON serialization. Frontend
shows a download fallback when content is absent.

F4: handleMarkdown now infers skill name from filename (same as
handleZip) when frontmatter has no name, instead of rejecting
with 400. Consistent behavior across import paths.

F1 (reviewer concern): upsertSkillFile is NOT affected — it uses
{ new: false } for insert-vs-replace detection but does a follow-up
findOne (lines 855-859) to return the post-upsert document.

* fix: deleteFile arg shape, raw URL base path, hoist SAFE_INLINE_MIMES

Codex P2: deleteFile expects { filepath } object, not a raw string.
Both upload handler cleanup and import handler cleanup now pass
{ filepath } to match the strategy contract (deleteLocalFile,
deleteFileFromS3 all expect a file object).

Codex P2: Raw download URL in SkillFileViewer now uses apiBaseUrl
prefix so subpath deployments (/chat, etc.) resolve correctly.

NIT: Hoist SAFE_INLINE_MIMES Set to factory scope — was re-allocated
per raw download request inside the if block.

* fix: Remove inert cache write for large text files, localize aria-label

N2: The { isBinary: false } cache write for text files > 1MB had no
effect — subsequent requests still fell through to stream read since
neither isBinary nor content provided a fast-path short-circuit.
Removed the pointless DB updateOne per request.

N4: Replace hardcoded "Back to skill" aria-label with localize().

* refactor: Extract shared parseFrontmatter, widen deleteFile type

N3: Extract parseFrontmatter into Skills/utils/frontmatter.ts —
single implementation shared by SkillDetail and SkillFileViewer.
Accepts optional skipKeys set so callers control which frontmatter
fields are excluded (SkillDetail skips name/description since
they're shown in the header; other .md files show all fields).

N5: Widen ImportSkillDeps.deleteFile file param from { filepath }
to { filepath; [key: string]: unknown } to signal extensibility
if strategies start accessing additional file properties.

* fix: Advance i past list items for skipped keys, DRY parseSkillMd

Finding A: parseFrontmatter now consumes multi-line YAML list items
before checking skipKeys — prevents list lines from leaking into
subsequent key parsing as spurious fields.

Finding B: parseSkillMd now delegates to the shared parseFrontmatter
instead of re-implementing the same frontmatter scanning loop.
Reduces client-side parseFrontmatter implementations from 3 to 1.

* fix: Call apiBaseUrl(), delete storage blob on file removal

- apiBaseUrl is a function, not a string — call it in the template
  literal so raw download URLs resolve correctly.
- deleteFileHandler now looks up the file record before deleting,
  then fire-and-forget deletes the storage blob via the strategy's
  deleteFile. Previously only the DB record was removed, leaving
  orphaned blobs in local/S3/Firebase/Azure storage.

* fix: Clean up storage blobs when deleting an entire skill

deleteHandler now lists all files for the skill before calling
deleteSkill, then fire-and-forget deletes each blob via the
storage strategy. Previously only per-file deletion cleaned up
blobs — deleting a whole skill left all associated files orphaned
in local/S3/Firebase/Azure storage.

* refactor: useImportSkillMutation hook, fix TSkill[] unsafe cast

- Create useImportSkillMutation in mutations.ts + ImportSkillOptions
  type. UploadSkillDialog now uses the mutation hook instead of
  calling dataService.importSkill directly with manual useState
  loading management. Eliminates unmounted-component state update
  risk and aligns with the React Query mutation pattern used by
  every other mutation in the codebase.

- SkillSelectDialog: replace as unknown as TSkill[] with proper
  TSkillSummary typing. SkillCard props updated to TSkillSummary.
  The dialog only uses summary-level fields (name, description,
  category, author) — the cast was hiding a type mismatch.

* fix: Use saved source for import cleanup, delete old blob on replace

Codex P2: Import cleanup now uses file.source (the backend the file
was actually saved to) instead of re-resolving from config. In mixed
strategy setups, the previous approach could target the wrong backend.

Codex P2: When re-uploading a file to an existing relativePath, the
old blob is now deleted after successful upsert. Previously only the
DB record was replaced, leaving the old storage object orphaned.

* fix: Register PUT /:roleName/skills route in roles router

* fix: Re-read skill after zip file processing for fresh metadata

The import response was built from the skill object created before
the file loop, but each upsertSkillFile bumps version and fileCount.
Clients caching the stale response would get 409 conflicts on first
edit and see incorrect file counts.

Now re-reads the skill via getSkillById after the loop so the
response reflects the current version, fileCount, and updatedAt.

* fix: Size-check SKILL.md before decompression, don't gate on fileCount

P1: SKILL.md was decompressed before any size accounting. A crafted
archive could expand SKILL.md past 10MB before validation ran. Now
checks declared size pre-decompression and actual size post, both
against MAX_SINGLE_FILE_BYTES.

P2: File list query was gated on cached fileCount which can be stale
after mutations. Now fetches files for the active skill regardless
of fileCount. hasFiles derived from fetched data with fileCount as
fallback, so newly uploaded files appear without hard refresh.

* fix: Move files declaration before hasFiles to avoid TDZ error

* security: Stream-decompress zip entries with enforced byte cap

Replace zipEntry.async('nodebuffer') (buffers entire entry before
checking limits) with zipEntry.nodeStream('nodebuffer') piped
through a byte counter that destroys the stream when the per-file
or cumulative limit is exceeded.

Previously, when JSZip's _data.uncompressedSize was absent (the
common case), a high-ratio entry could allocate hundreds of MB
before the post-decompression check caught it. Now decompression
is aborted mid-stream at the exact byte threshold — no entry can
exceed its limit regardless of compression ratio.

* refactor: Reorganize access check for prompts in useSideNavLinks hook

Moved the prompts access check to a new position in the code to improve readability and maintainability. This change ensures that the prompts link is added to the navigation only if the user has the appropriate access, without altering the existing functionality.

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-04-25 04:02:00 -04:00
..
a11y.yml 🤲 a11y: Sidebar Text Contrast (#3665) 2024-08-16 13:50:47 -04:00
backend-review.yml 📜 feat: Skills UI + Initial E2E CRUD / Sharing (#12580) 2026-04-25 04:02:00 -04:00
build.yml
cache-integration-tests.yml 🌊 feat: Resumable LLM Streams with Horizontal Scaling (#10926) 2025-12-19 12:14:19 -05:00
client.yml v0.8.1 (#10882) 2025-12-10 09:06:01 -05:00
data-provider.yml v0.8.1 (#10882) 2025-12-10 09:06:01 -05:00
data-schemas.yml v0.8.1 (#10882) 2025-12-10 09:06:01 -05:00
deploy-dev.yml 🐋 ci: update dev deployment script 2025-06-02 05:33:46 -04:00
deploy.yml chore: Get the latest of all github actions (#1335) 2023-12-14 07:44:38 -05:00
dev-branch-images.yml 🏗️ refactor: Extract DB layers to data-schemas for shared use (#7650) 2025-05-30 22:18:13 -04:00
dev-images.yml 🐳 experimental: Dev Image Workflow & Remove Unused Code (#1928) 2024-02-29 09:24:55 -05:00
dev-staging-images.yml 🐋 chore: Add Workflow for Docker Dev Staging Images Build 2025-11-27 12:26:37 -05:00
eslint-ci.yml 📡 refactor: MCP Runtime Config Sync with Redis Distributed Locking (#10352) 2025-12-11 16:36:15 -05:00
frontend-review.yml 📜 feat: Skills UI + Initial E2E CRUD / Sharing (#12580) 2026-04-25 04:02:00 -04:00
generate_embeddings.yml 🚀 feat: Add GitHub Actions Workflow for Generating Docs Embeddings (#2216) 2024-03-26 11:57:04 -04:00
gitnexus-cleanup-pr.yml 🌊 feat: Add GitNexus DigitalOcean Pipeline with PR Index Serving (#12612) 2026-04-11 13:04:46 -04:00
gitnexus-deploy.yml 🧹 chore: Cap PR Indexes at 3 and Add Delete-Before-Sync (#12672) 2026-04-15 09:46:48 -04:00
gitnexus-index.yml 🧹 chore: Cap PR Indexes at 3 and Add Delete-Before-Sync (#12672) 2026-04-15 09:46:48 -04:00
gitnexus-pr-command.yml 🧠 feat: Enable GitNexus Embeddings for Dev Branch and PR Indexes (#12620) 2026-04-11 13:35:29 -04:00
helmcharts.yml 🪖 ci: Helm OCI Publishing (#7256) 2025-08-11 16:21:05 -04:00
i18n-unused-keys.yml 🏷️ chore: Add Missing Localizations for Agents, Categories, Bookmarks (#9266) 2025-08-25 13:54:13 -04:00
locize-i18n-sync.yml 🌐 ci: Bump Locize i18n Action Version 2025-08-01 15:52:00 -04:00
main-image-workflow.yml 🐳 hotfix: Tag Images Workflow Update (#2272) 2024-04-01 19:04:37 -04:00
tag-images.yml 🐳 hotfix: Tag Images Workflow Update (#2272) 2024-04-01 19:04:37 -04:00
unused-packages.yml 🧵 refactor: Migrate Endpoint Initialization to TypeScript (#10794) 2025-12-11 16:37:16 -05:00