Skip to content

feat: skills management page, chat layout refresh and UI fixes - #158

Merged
aazf merged 33 commits into
mainfrom
feat/skills-browse
Oct 24, 2026
Merged

aazf merged 33 commits into
mainfrom
feat/skills-browse

Conversation

@cd996

@cd996 cd996 commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds a Skills page for managing agent skills from BKD. Also reworks the chat message layout and fixes several UI issues found along the way.

Skills (/skills)

Browse and edit

  • Lists global and project skills straight from disk, in the layout the skills CLI uses:
    • ~/.agents/skills together with its lock file;
    • ~/.claude/skills;
    • a project's own .agents/skills and .claude/skills.
  • Each skill shows its source and the agents that read it.
  • Paths resolve the way [email protected] does: $XDG_STATE_HOME for the lock, $CLAUDE_CONFIG_DIR for the Claude dir.
  • Files open in the workspace editor, with Markdown preview and a collapsible file tree.
  • Local skills can be created, edited, copied and deleted; delete moves them to data/trash/skills/.
  • Skills installed by the CLI are read-only, because skills update would overwrite local edits. "Copy to local skill" makes an editable copy.

Install, update, uninstall

  • These run npx [email protected] as an argv list, one operation at a time. Each operation has live output, a 5-minute timeout and can be cancelled. A dialog cannot be dismissed while its operation runs.
  • Install first previews the repository's skills and lets the user pick which to install.
  • Updates are checked only on request. An installed global skill with a GitHub source has a "Check for updates" button.
    • The check compares the lock's skillFolderHash with GitHub's tree API, at the ref the skill was installed from. Nothing is cached between checks.
    • Project skills cannot be checked this way, because the CLI records a different hash for them, so they offer "Update" directly.
  • Every update, single or "Update all", first opens a dialog where the user ticks which skills to update.
  • Upload accepts a SKILL.md or a .zip / .skill archive.
    • Every archive entry is checked before unpacking: no .. or absolute paths, at most 1000 files, at most 50 MB unpacked.
    • name and description are both required, and an existing skill is never overwritten.

Safety

  • A lock file that exists but cannot be read keeps every store skill read-only.
  • Project skills, the project's skill stores and its skills-lock.json must resolve inside the project. Symlinks that point outside are ignored or refused.
  • Copying a skill keeps inner symlinks as links instead of copying what they point to.
  • A failed create, copy or upload removes the half-made directory.
  • Project-scoped operations are refused when the project directory is HOME.

Chat

  • All chat blocks share one shape (chat-block.ts): one radius, one padding, one vertical rhythm.
    • Messages no longer have a thick leading bar. User and assistant messages are told apart by background.
    • Tool groups and command output keep a 1px outline.
  • User messages sit on the right as a bubble sized to their text.
  • Each assistant message has two actions:
    • a source/rendered toggle; the code icon is highlighted while the source shows;
    • a copy button that copies whatever is on screen.
  • Footer actions are always visible and use the same colour as the timestamp.
  • One-line summaries are cut by CSS at the row width instead of at a fixed character count: thinking, commands outside a tool group, and note previews. Every truncated line shows its full text on hover.
  • The turn navigator follows the latest turn on first load.

Other UI fixes

  • The review list matches the project list: same header height, same group bars, and the same right-click and "..." menus.
  • The sidebar rail and the mobile menu highlight the current page.
  • Header selects match the small button height and use SelectGroup.
  • The duplicate page-header logos are removed.
  • The mobile menu's close button sits in its header row.

Tests

  • Fixes two flaky tests:
    • patch-diff-options: the library was cold-imported inside the timed window.
    • The skills preview test: the status flipped to succeeded before the preview result was set.

Test plan

  • bun run check (lint, typecheck, 977 API and 375 frontend tests, build)
  • Manual check in dev:
    • Skills: check, update with confirmation, install, uninstall, upload, small screens.
    • Chat layout.
    • Review list.
    • Sidebar highlight.

cd996 added 9 commits October 22, 2026 05:38
A /skills page lists the agent skills on disk, opens their files in the
editor, and creates, edits, copies and deletes local skills. Part B of task
20261022-0307-skills-management; installing and updating through the
skills CLI follows separately.

Skills are read in the layout of the skills CLI (vercel-labs/skills): the
shared store .agents/skills (read by Codex; entries in .skill-lock.json are
managed) and .claude/skills (read by Claude Code: links into the store plus
real skill dirs), under HOME and under a project's directory. No engine CLI
is called. Managed skills are read-only because skills update overwrites
local edits; copying makes a local skill under a new name. New skills go to
.agents/skills with a relative link in .claude/skills so both engines see
them; deleting moves a local skill to data/trash/skills and drops its link.
Paths are realpath-checked inside the skill; these roots do not relax the
file browser's workspace check.
Install from a GitHub repository with a preview of its skills, update and
uninstall through the skills CLI (one operation at a time, live output),
and upload a SKILL.md or a .zip / .skill archive. Installed skills show
whether their repository has a newer version, comparing the lock's
skillFolderHash with GitHub's tree API.

The page adapts to small screens: the list or the open skill at full
width, with a collapsible file tree as in the workspace. Markdown files
preview with their frontmatter as YAML, and the delete dialog no longer
promises a restore the UI does not offer.
Each assistant message flips between rendered Markdown and its source in
place instead of opening a dialog. The turn navigator follows the latest
turn on first load and while scrolling.
Share the header height, the collapsible group bars and the issue rows,
so the review list gets the same right-click and overflow menus.
The desktop rail and the mobile menu highlight the global page being
viewed, including its nested routes. Header selects match the small
button height and group their items, page headers drop the duplicate
logo, and the mobile menu's close button sits in its header row.
The 1 s findByTestId window covered the cold lazy import of the library
on top of React's 300 ms Suspense reveal throttle, so the test failed
under load.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T03:13:23.695615Z 721e507 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 2d22199741

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/skills/cli.ts Outdated
Comment on lines +203 to +206
previewHome = await mkdtemp(join(tmpdir(), 'bkd-skills-preview-'))
await mkdir(join(previewHome, '.codex'), { recursive: true })
args = ['add', input.source!, '-s', '*', '-a', 'codex', '-g', '-y', '--json']
env = cliEnv({ HOME: previewHome })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Sandbox XDG state during repository previews

When the server has XDG_STATE_HOME set, overriding only HOME does not isolate the preview: the pinned CLI resolves its global lock from $XDG_STATE_HOME/skills/.skill-lock.json, so merely previewing a repository can add or overwrite entries in the real lock while the installed files are later deleted with the temporary home. Override XDG_STATE_HOME to a temporary directory as well. See the CLI's getSkillLockPath() implementation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 4193673: the preview now sets XDG_STATE_HOME inside the throwaway HOME, so the CLI's lock stays there. Covered by a test in api-skills-operations.test.ts.

Comment on lines +41 to +44
agents: join(home(), '.agents/skills'),
claude: join(home(), '.claude/skills'),
lockFile: join(home(), '.agents/.skill-lock.json'),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resolve the global lock from XDG_STATE_HOME

On hosts that set XDG_STATE_HOME, [email protected] reads and writes $XDG_STATE_HOME/skills/.skill-lock.json, but this code always reads ~/.agents/.skill-lock.json. Consequently CLI-installed directories are presented as unmanaged local skills, allowing the UI to edit or delete them and omitting their source/update metadata. Match the pinned CLI's getSkillLockPath() precedence.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 4193673: globalRoots() follows the CLI's getSkillLockPath(), using $XDG_STATE_HOME/skills/.skill-lock.json when set. It also follows CLAUDE_CONFIG_DIR for the Claude skills dir, which the CLI uses for claude-code's globalSkillsDir.

Comment thread apps/api/src/skills/updates.ts Outdated
const headers: Record<string, string> = { 'Accept': 'application/vnd.github+json', 'User-Agent': 'bkd' }
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN
if (token) headers.Authorization = `Bearer ${token}`
const res = await fetch(`https://api.github.com/repos/${source}/git/trees/HEAD?recursive=1`, {

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 Compare updates against the recorded Git ref

For a skill installed from a tag or non-default branch, the lock entry records a ref, but every check here fetches HEAD. This can report an unchanged pinned skill as updated or removed, and sources with multiple refs also collide in the source-only cache. Preserve the lock's ref and fetch/cache the corresponding tree; the pinned CLI's lock schema includes the installation ref.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 03ad599: the lock's ref is passed to the tree fetch (HEAD when absent), and the cache is keyed by source and ref.

Comment on lines +240 to +243
<SkillDetail
key={skillKey(selected)}
skill={selected}
projectId={projectId || undefined}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Key project skill editors by project

If two projects contain a project skill with the same location/name and the destination project's query is already cached, switching the project picker reuses this SkillDetail instance because its key is unchanged. Its drafts state from the first project is then displayed for the second project, and Save writes that draft into the second project's skill. Include projectId in the key for project-scoped skills or clear selection/editor state on project changes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in a2ceb17: project skills are now keyed by project as well, so switching projects remounts the editor. Covered by a test in skills-page.test.tsx.

Comment on lines +1 to +4
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect } from 'react'
import { kanbanApi } from '@/lib/kanban-api'
import type { SkillLocation } from '@/types/kanban'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Place the new React Query hooks in use-kanban.ts

This introduces the skills endpoint wrappers in a separate hook module, while the repository convention requires new kanban-api.ts endpoints to be wrapped in use-kanban.ts. Keeping another independent query-key and invalidation layer makes the established server-state entry point incomplete; move or re-export these hooks through the mandated module.

AGENTS.md reference: AGENTS.md:L230-L231

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not changing this one. The repo already keeps feature hooks in their own modules (use-messages.ts, use-notes.ts), and use-skills.ts follows that pattern with its own key factory. The AGENTS.md line predates those modules.

cd996 added 3 commits October 23, 2026 04:01
[email protected] keeps its global lock under $XDG_STATE_HOME when set and
installs Claude Code skills under $CLAUDE_CONFIG_DIR. Reading only the
HOME paths showed CLI-installed skills as editable local ones on such
hosts, and a repository preview wrote into the real lock. The preview now
gets its own XDG_STATE_HOME inside the throwaway HOME.
A skill installed from a branch or tag records it as the lock's ref;
comparing it with the default branch reported pinned skills as updated
or removed. Fetch and cache the tree per source and ref.
Two projects can hold a project skill with the same name. Switching the
project kept the editor mounted, so its unsaved draft showed for the
other project's skill and Save wrote it there.

@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: a2ceb17915

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/skills/service.ts Outdated
Comment on lines +103 to +104
} catch {
return {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Fail closed when the lock cannot be read

When an existing CLI lock is malformed or temporarily unreadable, this returns an empty object, so scan() marks every CLI-installed directory as unmanaged. The save and delete endpoints then pass assertEditable() and can overwrite or move skills that are still owned by the CLI; distinguish a genuinely missing lock from parse or permission failures, or otherwise keep those skills read-only.

Useful? React with 👍 / 👎.

Comment thread apps/api/src/skills/upload.ts Outdated
Comment on lines +54 to +58
function pickName(skillMd: string, fallback: string): string | null {
const fromMeta = parseFrontmatter(skillMd).name?.trim()
if (fromMeta && NAME_RE.test(fromMeta)) return fromMeta
const base = fallback.replace(/\.(zip|skill|md)$/i, '')
return NAME_RE.test(base) ? base : 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 Validate uploaded SKILL.md frontmatter

When an uploaded .md has no valid name, this falls back to the filename without modifying or rejecting the file, and it never requires a description; the API therefore reports success for a directory whose SKILL.md the pinned CLI will skip because both fields are required by its parseSkillMd() validation. Validate both fields before installation, or rewrite the accepted fallback name into valid frontmatter.

Useful? React with 👍 / 👎.

Comment thread apps/api/src/skills/service.ts Outdated
}
if (skill.location.endsWith('agents')) {
// <base>/.agents/skills/<name> -> <base>/.claude/skills
const claudeDir = join(dirname(dirname(dirname(skill.path))), '.claude/skills')

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 Remove links from CLAUDE_CONFIG_DIR on delete

When CLAUDE_CONFIG_DIR is configured, creation places the Claude symlink under that configured directory, but deletion reconstructs ~/.claude/skills from the canonical agent path instead. Deleting a global local skill consequently leaves a dangling link in the real Claude directory, and recreating the same name later fails with a conflict because placeNewSkill() still sees that link; derive this directory from globalRoots().claude for global skills.

Useful? React with 👍 / 👎.

@cd996
cd996 requested a review from aazf October 23, 2026 07:03
cd996 added 3 commits October 23, 2026 07:07
…NFIG_DIR

A lock file that exists but cannot be read or parsed was treated as
empty, so CLI-installed skills became editable and deletable. Keep every
store skill read-only until the lock can be read again.

Deleting a global skill now removes its link from the Claude dir it was
created in, which is CLAUDE_CONFIG_DIR when that is set; before, the
link was left dangling and blocked recreating the name.
The skills CLI ignores a SKILL.md without both fields, so an upload that
fell back to the file name produced a skill no engine would load. Skip
such files and report why.
Success cleared the selection but left the dialog state on 'uninstall',
so selecting any skill afterwards reopened the dialog for it. Close the
dialog on success, and reset skill dialogs whenever a skill is selected.

@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: d1927d276d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +160 to +163
for (const entry of await readEntries(roots.agents)) {
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
const dir = join(roots.agents, entry.name)
const meta = await readDescription(dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep project skill symlinks within the project root

When a project checkout contains .agents/skills/name as a symlink to another directory containing SKILL.md, this accepts it as a project skill. resolveInside() subsequently realpaths that external directory and treats it as the allowed root, so the project-scoped read and save endpoints can expose or overwrite files outside the project, including another project's skill directory. Reject project-store symlinks or verify their resolved target remains under the project's skill root.

AGENTS.md reference: AGENTS.md:L234-L234

Useful? React with 👍 / 👎.

Comment on lines +134 to +135
async function validate(input: StartOperationInput): Promise<void> {
if (input.scope === 'project' && !input.projectDir) throw new SkillError('Project has no directory', 400)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject project CLI operations when the project is HOME

When a project's directory resolves to HOME, project-scoped add/update operations are still accepted even though listSkills() deliberately suppresses that project's store because it aliases the global store. An add therefore writes into the global skill directory with a project lock, after which the UI presents the installed skill as unmanaged and permits editing or deletion; remove validation also cannot find it as a managed project skill. Apply the existing assertProjectScope() guard to CLI operations before starting them.

Useful? React with 👍 / 👎.

Comment on lines +103 to +109
<Sheet
open={open}
onOpenChange={(next) => {
if (!next) {
reset()
setSource('')
}

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 Preserve running operations when the install sheet closes

If the user dismisses the sheet with its default close button or Escape while a preview or install is running, this clears both operation IDs without cancelling the server operation. The operation continues to hold the global one-at-a-time slot for up to five minutes, but reopening the sheet cannot poll or cancel it and subsequent starts only receive a 409. Either prevent dismissal while running, cancel on close, or retain the ID so the operation remains reachable.

Useful? React with 👍 / 👎.

Comment thread apps/api/src/skills/updates.ts Outdated
Comment on lines +55 to +60
.then((t) => {
const map = new Map<string, string>([['', t.sha]])
for (const e of t.tree) {
if (e.type === 'tree') map.set(e.path, e.sha)
}
return map

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 Treat truncated GitHub trees as unknown

For sufficiently large repositories, GitHub can return a successful recursive tree response marked truncated; this mapping ignores that marker and treats every omitted folder as absent. A managed skill whose folder falls outside the partial response is consequently reported as removed even though it still exists. Carry the truncated field through the response and return unknown, or fetch the relevant subtree before declaring removal.

Useful? React with 👍 / 👎.

cd996 added 4 commits October 23, 2026 07:34
A checkout can link .agents/skills/<name> to any directory holding a
SKILL.md. Listing it let the project-scoped read and save routes reach
files outside the project, such as another project's or a global skill.
Project skills must now resolve inside their project.
Such a project's store is the global one, so a project-scoped add wrote
into the global store under a project lock and the skill then showed as
editable. Apply the same guard as create and upload.
…known

GitHub can return a partial recursive tree for a large repository; a
folder absent from it may still exist, so it no longer counts as removed.
Closing the install sheet, or the update or uninstall dialog, dropped the
operation id while the server kept running it in its only slot, so it
could be neither followed nor cancelled and new operations got 409. The
close button is hidden and dismissal ignored until the operation ends or
is cancelled.

@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: 2c34f2b21d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/skills/service.ts Outdated
Comment on lines +193 to +194
for (const entry of await readEntries(roots.claude)) {
if (!entry.isDirectory()) continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject symlinked Claude stores before scanning

When .claude/skills itself is a symlink, readdir() reports the target's children as ordinary directories, so this loop classifies them as unmanaged Claude-only skills. If it points at .agents/skills, managed skills gain an editable/deletable alias; for a project, a link outside the checkout exposes external skill directories through project-scoped read, write, and delete routes. Resolve and validate the Claude store root, deduplicate aliases of the agents store, and apply the project confinement check here as well.

AGENTS.md reference: AGENTS.md:L234-L234

Useful? React with 👍 / 👎.

Comment thread apps/api/src/skills/service.ts Outdated
const location: SkillLocation = skill.scope === 'global' ? 'global-agents' : 'project-agents'
const { roots } = rootsFor(location, projectDir)
await placeNewSkill(roots, newName, async (dir) => {
await cp(await realpath(skill.path), dir, { recursive: true, dereference: true })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve nested symlinks when copying a skill

When a skill contains an inner symlink to a file or directory outside its root, dereference: true follows that link and materializes the external content in the new editable skill, even though the normal file APIs reject the same traversal. A project skill can therefore copy another project's files or host files into an API-readable directory; preserve nested links or reject any link whose resolved target leaves the source skill before copying.

AGENTS.md reference: AGENTS.md:L234-L234

Useful? React with 👍 / 👎.

Comment thread apps/api/src/skills/service.ts Outdated
Comment on lines +321 to +323
await fill(dir)
await mkdir(roots.claude, { recursive: true })
await symlink(relative(roots.claude, dir), link)

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 Roll back the directory when skill placement fails

If fill() fails partway through, or if creating the Claude symlink fails because of permissions or a race, the agents directory remains on disk even though the API reports an error. The partial skill may then appear in the list, and every retry fails with a name conflict; wrap placement in cleanup that removes the newly created directory and link on failure.

Useful? React with 👍 / 👎.

…ed creates

- A .claude/skills linked wholesale to .agents/skills is one store: its
  entries are no longer listed again as editable Claude-only skills, and
  new skills skip the extra link that would collide.
- Project .claude/skills entries must resolve inside the project, like
  .agents/skills entries.
- Copy keeps inner symlinks as links instead of copying what they point
  to from outside the skill.
- A create, copy or upload that fails midway removes its half-made
  directory, so the name is not blocked on retry.

@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: 90e3f85cb5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +245 to +247
export async function assertProjectScope(scope: 'global' | 'project', projectDir?: string | null): Promise<void> {
if (scope === 'project' && projectDir && await isHomeDir(projectDir)) {
throw new SkillError('This project\'s directory is your home directory, so its skills are the global ones: use the global scope', 400)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Confine project skill stores before mutation

When a checkout makes .agents/skills or .claude/skills a symlink outside the project, this guard still accepts the scope; create, copy, upload, and CLI-add operations then follow that store and write files or links into another project or host directory. The scan-time confinement only hides those entries afterward, so resolve both project store roots and reject roots outside projectDir before allowing any project-scoped mutation.

AGENTS.md reference: AGENTS.md:L234-L234

Useful? React with 👍 / 👎.

Comment thread apps/api/src/openapi/schemas.ts Outdated
export const CreateSkillSchema = z.object({
scope: z.enum(['global', 'project']),
name: skillName,
description: z.string().min(1).max(1024),

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 Reject whitespace-only skill descriptions

When an API client submits a description containing only whitespace, this schema accepts it and createSkill() trims it to an empty string before writing description: "". The upload path rejects the same empty metadata, so creation can report success for a skill that does not satisfy the required description contract; trim before applying the minimum-length check.

Useful? React with 👍 / 👎.

Comment thread apps/frontend/src/pages/SkillsPage.tsx Outdated
const repo = sourceHref(sourceUrl)
if (!repo || new URL(repo).host !== 'github.com') return null
if (!sourcePath) return repo
return `${repo.replace(/\/+$/, '')}/tree/HEAD/${sourcePath.split('/').map(encodeURIComponent).join('/')}`

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 Link installed skills to their recorded Git ref

For a skill installed from a tag or non-default branch, this always constructs the folder URL using HEAD, even though the lock records the installation ref. Clicking “Open on GitHub” can therefore open unrelated content or a 404; carry the ref through SkillInfo and use it in this URL.

Useful? React with 👍 / 👎.

cd996 added 3 commits October 23, 2026 08:23
…alled ref

- Project-scoped writes are refused when the project's .agents or
  .claude/skills store links outside the project; before, create, copy,
  upload and CLI add followed the link.
- A whitespace-only description no longer passes create validation.
- Skills carry the ref they were installed from (sourceRef), and
  "Open on GitHub" uses it instead of HEAD.
The status flipped to succeeded before the repository's SKILL.md files
were read, so a poll in between saw a finished preview with no skills.
The preview test failed intermittently under load for this reason.
The thinking summary was cut at 80 characters, a command outside a tool
group at 90 and a note preview at 80, so on a wide screen the text
stopped short of the row. They now render one line and let CSS truncate
it, like the grouped command summaries.

@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: 40e4965e50

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/skills/updates.ts Outdated
Comment on lines +85 to +86
if (!entry?.source || !SOURCE_RE.test(entry.source) || (entry.sourceType && entry.sourceType !== 'github') || !entry.skillPath || !entry.skillFolderHash) {
return { ...base, status: 'unknown' }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Handle the project lock's computedHash

For every project-scoped CLI install, the pinned CLI records computedHash rather than skillFolderHash in skills-lock.json (see the official LocalSkillLockEntry). This condition therefore returns unknown for all such skills, so the UI never exposes their Update button even when an update exists. Project locks need their own comparison strategy instead of applying the global-lock tree-hash schema.

Useful? React with 👍 / 👎.

Comment on lines +255 to +256
for (const store of [roots.agents, roots.claude]) {
let probe = store

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Confine the project lock before running the CLI

When a checkout contains skills-lock.json as a symlink to a writable file outside the project, this guard accepts it because it checks only the two skill stores. Project add/update/remove then run the CLI with the checkout as cwd, and the pinned CLI's writeLocalLock() writes that path directly, following the symlink and overwriting the external target. Reject a project lock path that resolves outside the checkout before starting project-scoped CLI operations.

AGENTS.md reference: AGENTS.md:L234-L234

Useful? React with 👍 / 👎.

cd996 added 3 commits October 23, 2026 09:36
- Every chat block shares one shape (chat-block.ts): one radius, one
  padding, one vertical rhythm. Messages lose the thick leading bar and
  are told apart by background; tool groups and command output keep a
  1px outline.
- User messages sit on the right as a bubble sized to their text, tinted
  with the primary colour.
- An assistant message has two actions: switch between rendered and
  source (the code icon is highlighted while the source shows) and copy,
  which copies what is on screen. Footer actions stay visible and share
  the timestamp colour.
- Every truncated line in the chat carries its full text as a title.
…s outside

The skills CLI writes skills-lock.json in place, following a symlink
(even a dangling one), so a checkout could make a project operation
overwrite a file outside the project.
- Nothing is checked when the page opens. An installed global skill with
  a GitHub source has a "Check for updates" button; the API checks one
  skill via ?location=&name= and no longer caches between checks.
- Project locks record a content hash GitHub cannot be compared with, so
  project skills are not checked and offer Update directly.
- Every update, single or "Update all", first opens a dialog listing the
  scope's installed skills to tick; skills known to have an update come
  pre-selected, and the CLI runs only for the ticked ones.
@cd996 cd996 changed the title feat(skills): manage agent skills from the UI feat: skills management page, chat layout refresh and UI fixes Oct 23, 2026

@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: dce64bab06

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/skills/service.ts Outdated
if (confine && !(await resolvesInside(dir, confine))) continue
const meta = await readDescription(dir)
if (!meta) continue
const locked = lock?.[entry.name]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Match lock entries by the CLI's sanitized install name

When a CLI-installed skill has an uppercase or otherwise sanitized name, the pinned CLI stores it in a lowercased/sanitized directory while preserving the original name as the lock key; this exact lookup therefore misses the entry. For example, installing MySkill creates myskill/ but leaves the lock keyed by MySkill, so BKD presents the installation as unmanaged and permits editing or deleting it while omitting its source/update metadata. Match entries using the CLI's sanitization rules; its resolveSkillsToRemove() documentation explicitly describes this folder-name/lock-key mismatch.

Useful? React with 👍 / 👎.

Comment thread apps/frontend/src/pages/SkillsPage.tsx Outdated
Comment on lines +303 to +304
// Remount per opening, so the ticks start from this opening's pre-selection
key={`${updatePick.scope}:${updatePick.preselected.join(',')}`}

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 Reset the update picker every time it opens

When the user changes selections, cancels, and later reopens the same scope with the same preselection, this key is unchanged and updatePick was never cleared, so the still-mounted dialog retains its previous picked state. The user can consequently confirm skills they thought were deselected by cancelling; use an opening nonce, clear updatePick on close, or synchronize the picker state when it opens.

Useful? React with 👍 / 👎.

cd996 added 2 commits October 23, 2026 09:50
[email protected] keys the lock by a skill's own name but names its folder
after sanitizeName(name), so `My Skill` lives in `my-skill/`. The exact
lookup missed such entries and showed CLI-installed skills as editable
local ones without source or update status. Lookups now match the
sanitized name, and update sends the lock key, which is what the CLI
matches update names against.

Also skip starting the CLI when the operation was cancelled while it was
being prepared: the abort had already fired, so the runner would never
see it and the operation hung.
Reopening the dialog for the same scope and pre-selection kept the
ticks of a cancelled pick, so the user could confirm skills they had
deselected. Each opening now remounts it.

@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: d93c823915

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

await cp(await realpath(skill.path), dir, { recursive: true, verbatimSymlinks: true })
const skillMd = join(dir, 'SKILL.md')
const text = await readFile(skillMd, 'utf8')
await writeFile(skillMd, text.replace(/^(---\r?\n[\s\S]*?)^name:.*$/m, `$1name: ${newName}`), 'utf8')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject symlinked SKILL.md before rewriting the copy

When a project skill's SKILL.md is a symlink to a writable file outside the checkout, verbatimSymlinks: true preserves that link in the destination and this writeFile then follows it, overwriting the external file's name: field; the copied skill also remains linked outside its root. Reject such a link or materialize it only after verifying its resolved target is inside the source skill.

AGENTS.md reference: AGENTS.md:L234-L234

Useful? React with 👍 / 👎.

<div>
{issues.map(issue => (
<ReviewIssueRow
<IssueRow

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 Invalidate the review query after row deletion

When an issue is deleted through the menu newly inherited from IssueRow, useDeleteIssue invalidates only the project issue list, while the delete route emits no issue-updated SSE event. Consequently useReviewIssues retains the deleted row—even after the active issue navigates back to /review—until an unrelated refetch; invalidate queryKeys.reviewIssues() after deletion.

Useful? React with 👍 / 👎.

cd996 added 3 commits October 23, 2026 10:05
Copy keeps inner symlinks and then rewrites SKILL.md's name, so a
SKILL.md linked to a file elsewhere was rewritten in place. Such a link
is refused, and a SKILL.md linked inside the skill becomes a real file
in the copy before the rewrite.
The review list gained the issue row menu, but deleting refreshed only
the project's issues, and the delete route emits no issue-updated event,
so the deleted row stayed in the review queue.
With no outline or tint, the padding only pushed the reply in from the
footer and tool rows; without it they line up.

@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: 65904f0cae

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

function pickName(skillMd: string): string | null {
const meta = parseFrontmatter(skillMd)
const name = meta.name?.trim()
if (!name || !NAME_RE.test(name) || !meta.description?.trim()) 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 Reject upload names before writing the skill

When uploaded frontmatter uses a name such as foo..bar, this local regex accepts it, but the shared assertSkillName() later rejects names containing ... By then placeNewSkill() has already created both the agents directory and Claude link, so getSkill() returns a 400 after mutating disk; the upload appears to fail and leaves a skill the API cannot open or delete. Apply the same name validator before placement or roll back when the post-placement lookup fails.

Useful? React with 👍 / 👎.

*/
export function findLockEntry(lock: Record<string, LockEntry> | null | undefined, folder: string): [string, LockEntry] | undefined {
if (!lock) return undefined
if (lock[folder]) return [folder, lock[folder]]

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 Require an own property when matching lock entries

When no lock entry exists and a local skill is named constructor, toString, or another inherited property, lock[folder] resolves a truthy Object.prototype member. Scanning consequently marks the unmanaged skill as managed/read-only, preventing API edits and deletion while exposing bogus lock metadata. Check Object.hasOwn(lock, folder) before returning the exact match.

Useful? React with 👍 / 👎.

onChange={value => setDrafts(prev => ({ ...prev, [path]: value }))}
onSave={handleSave}
wrap={wrap}
preview={preview && isMarkdown(path)}

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 Route relative images through the skill file API

When a skill Markdown file references a bundled image with a relative URL, enabling this preview leaves the <img> relative to the /skills page because this FileEditor has no asset root and there is no raw skill-file route. Although uploaded and copied skills preserve their asset folders, those images cannot load in rendered preview; provide a confined raw-file URL for the selected skill and pass its base into the renderer.

Useful? React with 👍 / 👎.

@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: d2ac394a99

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +371 to +374
if (await exists(dir) || await exists(link)) throw new SkillError('A skill with this name already exists', 409)
await mkdir(roots.agents, { recursive: true })
try {
await fill(dir)

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 Serialize creation of the same skill name

When two create, copy, or upload requests target the same name concurrently, both can pass this existence check and write the same directory. The request that loses the Claude-link race then enters the cleanup path and recursively removes the directory written by the successful request, potentially leaving a dangling link and causing both responses to fail. Reserve the name atomically or serialize placement per store/name before filling and cleaning it up.

Useful? React with 👍 / 👎.

<div className="space-y-1.5">
<Label htmlFor="skill-source">{t('skills.repository')}</Label>
<div className="flex gap-2">
<Input id="skill-source" value={source} onChange={e => setSource(e.target.value)} placeholder="owner/repo" disabled={running} />

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 Reset the preview when the repository source changes

After a preview succeeds, this input becomes editable again while the old skill selection remains visible. If the user changes the repository and clicks Install without previewing again, runInstall() sends the new source with skill names and descriptions selected from the old source, so it can install an unintended same-named skill or fail unexpectedly. Clear the preview on source changes or keep the previewed source immutable until installation finishes.

Useful? React with 👍 / 👎.

Comment thread apps/api/src/skills/cli.ts Outdated
Comment on lines +279 to +280
const dir = join(previewHome, '.agents/skills', r.name)
const text = await readFile(join(dir, 'SKILL.md'), 'utf8').catch(() => '')

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 Read preview metadata from the CLI-reported path

For a skill whose name the pinned CLI sanitizes for its directory—for example, MySkill being installed under myskill—this constructs a nonexistent path from the unsanitized result name and silently returns an empty description. The preview therefore loses metadata for otherwise installable skills; use the result's path or apply the same folder-name sanitization used elsewhere.

Useful? React with 👍 / 👎.

- Update check: a lock entry with a content sha256 (recorded by the CLI
  when GitHub's tree API was unavailable at install) cannot be compared
  with a tree hash; report it as unknown instead of always "update".
- Preview reads each skill from the CLI's sanitized folder name, so a
  skill named `My Skill` gets its description; install accepts the name
  the repository declares and only refuses flag-like or control names.
- The install sheet, upload, create and copy dialogs reset on every way
  of closing, not only on dismissal, so reopening starts afresh.
- The delete dialog shows a failure and disables its action while the
  request runs.
- Drop stale comments and an invalidation of a query that no longer
  exists.
@aazf
aazf merged commit 21ab212 into main Oct 24, 2026
5 checks passed

@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: 721e5071d1

ℹ️ 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 (skill.managed || !dirty) return
save.mutate(
{ location: skill.location, name: skill.name, path, content, projectId },
{ onSuccess: () => setDrafts(({ [path]: _, ...rest }) => rest) },

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 Preserve edits made while a skill file is saving

If the user continues typing after clicking Save but before the request completes, the success callback unconditionally removes the file's draft. The editor remains editable during the request, so those newer changes disappear and the editor reverts to the older content returned by the save. Only clear the draft if its current content still matches the submitted content; otherwise retain it as unsaved, as the workspace file editor already does.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants