Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions docs/design/daemon-extension-skill-catalog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Daemon extension Skill catalog

This implements stage 2 of #11274. The daemon-local workspace Skill provider
currently supplies an empty active-extension list, so its first response omits
installed extension Skills when no child snapshot exists.

Use an unbound `ExtensionManager` for the selected workspace to load installed
extensions through the existing consistent store reader. Supply active
extensions to `SkillManager`, preserving project > user > extension > bundled
precedence. Append inactive extension Skills as management entries with the
existing `inactive_extension` status, retaining their identity and metadata.
Resolve settings and extension Skill defaults/overrides with the existing
parsers. A settings opt-in does not enable an inactive parent extension.
Resolve localized extension names with the existing language setting and locale
helpers, without changing the daemon process language. The resolved locale is
part of the provider cache key — recomputed from freshly loaded settings on
every call — because a language change reaches no invalidation point.

Keep the lightweight Config surface: do not construct a runtime Config, start a
child, initialize MCP, execute hooks, or install watchers. Honor safe mode,
disabled discovery levels and workspace trust; inert untrusted inventory must
not load workspace settings or extension runtime context.
`skills.disabledLevels` gates extension _discovery_ only; inactive-extension
management entries are appended regardless, matching the child producer.

Failure handling is two-tier. An unreadable extensions _root_ (or a configured
skills base directory) keeps the documented all-or-nothing behavior: an
uninitialized error status. The explicit `readdir` probe is what surfaces that
state — the store loader swallows listing errors, so without the probe an
unlistable root would silently yield an initialized catalog missing every
extension Skill. A fault _inside_ the extension load itself (corrupt store
state, a dangling extension symlink, lock contention) degrades only the
extension entries instead: the catalog is served initialized with the project,
user and bundled Skills, the failure is logged to the daemon's stderr, and the
degraded build is not cached so the next read retries extension enumeration.

Cache installs are guarded by a per-workspace invalidation epoch, so an
invalidation delivered while a cold build is in flight cannot be undone by
that build, and concurrent cold builds of one workspace are coalesced. Startup
`--enable-extension` overrides are not propagated here (the daemon's child
spawn generally does not carry them either).

The implementation and collocated regressions live in the daemon-local provider.
Tests cover real manifests, active/inactive state, source collisions, persisted
Skill settings, safe/untrusted contexts, explicit and mid-build cache
invalidation, language changes, the extension-load failure domain, unreadable
directory roots and discovery-level gating. E2E evidence uses an isolated home
and a daemon with no child session.

The facade still prefers child snapshots in this stage. Replacing that source,
changing toggle/refresh semantics, adding configured-state fields and changing
Web Shell projections belong to later PRs. No public schema changes are needed.

Known later-stage items (recorded during review, deliberately not in this
stage):

- The sibling `/workspace/extensions` route resolves its locale through a
settings load that admits the workspace `.env` into the daemon's
process-global environment, so a workspace `QWEN_CODE_LANG` can diverge the
two panels of one page. This provider follows the documented daemon
convention (`skipLoadEnvironment`); fixing the leak belongs to that route.
- The inactive-append and sort assembly is a second copy of the child's
(`acpAgent.ts`); extracting it into the shared
`runtime/workspace-skills-mapping.ts` module — keeping the child's
`level:extensionName:name` dedupe key — belongs to a follow-up.
- Retaining the cached `ExtensionManager` across skill-settings invalidations
behind `refreshCacheIfSourcesChanged` needs invalidation provenance
(extension-store mutation vs skill-settings mutation) that the current
call sites do not carry.
- The active-Skill `enabled` judgment mirrors `Config.isSkillEnabled`;
sharing that decision with core config belongs to a follow-up.
4 changes: 3 additions & 1 deletion packages/cli/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ function interpolate(
}

// Language setting helpers
function resolveLanguage(lang: SupportedLanguage | 'auto'): SupportedLanguage {
export function resolveLanguage(
lang: SupportedLanguage | 'auto',
): SupportedLanguage {
if (lang === 'auto') {
return detectSystemLanguage();
}
Expand Down
Loading
Loading