Skip to content

feat(desktop): skill management — create, edit, delete from Settings UI#2271

Open
HUQIANTAO wants to merge 9 commits into
esengine:mainfrom
HUQIANTAO:feat/skill-management
Open

feat(desktop): skill management — create, edit, delete from Settings UI#2271
HUQIANTAO wants to merge 9 commits into
esengine:mainfrom
HUQIANTAO:feat/skill-management

Conversation

@HUQIANTAO
Copy link
Copy Markdown
Contributor

Summary

Upgrade the Skills settings page from read-only to full CRUD with a markdown editor modal.

Changes

Protocol (desktop/src/protocol.ts)

  • New outgoing commands: skill_read, skill_save, skill_delete
  • New incoming event: $skill_detail (returns skill body for editing)
  • New SkillDetailEvent type

Node.js sidecar (src/cli/commands/desktop.ts)

  • Handle skill_read: uses SkillStore.read() + readFile() to return skill content
  • Handle skill_save: validates frontmatter, writes to folder or flat layout, re-emits $skills
  • Handle skill_delete: removes folder or flat file, re-emits $skills
  • Added imports: mkdir, rm, writeFile from node:fs/promises, homedir, dirname, SKILLS_DIRNAME, SKILL_FILE, validateSkillFrontmatter

Frontend (desktop/src/ui/settings.tsx)

  • PageSkills upgraded with:
    • "New skill" button in section header
    • Edit (pencil) and delete (trash) action buttons on each project/global skill card
    • Editor modal: name field, scope selector (project/global), markdown textarea
    • Delete confirmation: inline popover with cancel/confirm buttons (matches session delete pattern)
    • Built-in skills remain read-only (no edit/delete buttons)
  • SettingsModal receives new props: skillDetail, onSkillRead, onSkillSave, onSkillDelete

State (desktop/src/App.tsx)

  • skillDetail field added to State type and initial state
  • $skill_detail event handler in applyIncomingRaw
  • Pass new callbacks to SettingsModal

CSS (desktop/src/styles.css)

  • .skill-create-btn — accent button with hover inversion
  • .skill-actions — flex container for action buttons
  • .skill-action-btn — icon button with hover states
  • .skill-delete-confirm — inline danger popover
  • .skill-editor-mask — modal backdrop with fade-in
  • .skill-editor — modal panel with rise animation
  • .skill-editor-fields, .skill-editor-body, .skill-editor-actions — form layout

i18n

  • 13 new keys in en.ts and zh-CN.ts
  • de.ts / ja.ts / ru.ts auto-inherit via ...en spread fallback

- Add skill_read/skill_save/skill_delete RPC commands to protocol
- Handle new commands in desktop.ts (Node.js sidecar) using SkillStore
- Upgrade PageSkills from read-only to full CRUD:
  - 'New skill' button opens editor modal
  - Edit (pencil) and delete (trash) action buttons on project/global skills
  - Editor modal with name, scope, body (markdown) fields
  - Delete confirmation popover (inline, like session delete)
- Add  incoming event for loading skill content
- Add skillDetail state to App reducer
- Add i18n keys (en, zh-CN; de/ja/ru auto-fallback)
- Add CSS styles for editor modal, action buttons, delete confirm
Comment thread src/cli/commands/desktop.ts Fixed
targetPath = flatPath;
}
mkdirSync(dirname(targetPath), { recursive: true });
writeFileSync(targetPath, msg.body, "utf8");
@esengine
Copy link
Copy Markdown
Owner

CI is failing on biome formatter only — run npm run lint (the project's pre-commit hook does this) and push the formatted version. Once that's green I'll approve. The CRUD design looks right on a read-through.

- Add skill_read/skill_save/skill_delete to DesktopIncomingMsg union
- Add SkillDetailEvent interface
- Add SkillDetailEvent to EmittableEvent union
@esengine
Copy link
Copy Markdown
Owner

esengine commented May 29, 2026

CI lint is now green — thanks for the quick fix. One thing before I approve: msg.name is fed directly into the path joins in all three new handlers without validation:

const folderPath = join(dir, msg.name, SKILL_FILE);
const flatPath = join(dir, `${msg.name}.md`);

A name like ../../etc/foo would resolve outside the skills dir, and skill_delete then does rmSync(..., { recursive: true, force: true }) on it — that's a sharp edge if anything ever feeds untrusted input through IPC.

Please add a guard: reject msg.name that contains path separators or .., or compare path.resolve(targetPath) against path.resolve(dir) with startsWith to make sure it stays inside. The CodeQL TOCTOU hit at line 3119 (existsSync → writeFileSync race) is low impact for a single-user desktop — you can leave that one.

Once name validation is in I'll approve.

- Reject names containing '..', '/', or '\'
- For save/delete: verify resolved target path stays inside skills directory
- Prevents rmSync/writeFileSync from operating outside the skills dir
@HUQIANTAO
Copy link
Copy Markdown
Contributor Author

Fixed. Added two layers of validation:

  1. Name check: reject msg.name containing .., /, or \\ — applied in both skill_save and skill_delete handlers before any path operations.

  2. Resolved path check: after constructing the target path, verify resolve(targetPath) starts with resolve(dir) — catches any edge cases the name check might miss.

Applied to both skill_save and skill_delete. skill_read goes through SkillStore.read() which already validates via isValidSkillName() and resolves across known roots, so it's safe by design.

@esengine
Copy link
Copy Markdown
Owner

The validation looks good — .. / separator reject plus resolve().startsWith() defense-in-depth is exactly the right shape. Thanks.

CI is still red on biome though — two auto-fixable issues:

  • useTemplate rule wants \${resolvedDir}/`instead ofresolvedDir + "/"` (a few places)
  • formatter wants different indentation on the new emit lines

Easiest path: run npm run lint locally — the project's biome config will auto-apply both. Or npx biome check --apply src/cli/commands/desktop.ts for just that file. Push the result and CI should go green.

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.

3 participants