Skip to content

feat(ui): Skills management surface — unhide and rebuild the Skills tab (ent#235) - #1877

Merged
vybe merged 2 commits into
devfrom
feat/235-skills-ui
Jul 29, 2026
Merged

feat(ui): Skills management surface — unhide and rebuild the Skills tab (ent#235)#1877
vybe merged 2 commits into
devfrom
feat/235-skills-ui

Conversation

@dolho

@dolho dolho commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Implements trinity-enterprise#235. OSS-core, ungated — confirmed with the issue author before building: every file here is already public, the endpoints are already ungated, and the paid piece (skill_runner, ent#139) plus exposure curation (#178) are both explicitly out of scope.

The gap

The skills machinery shipped across three planes (#182 distribute/place/expose, #183 package injection with a per-skill result contract) and nothing rendered any of it. The tab was excluded from visibleTabs per requirements §22.2 ("component preserved for potential admin-only access"), assignment was REST/MCP-only (§21.3), and #183's statuses and named warnings had no consumer. A user could not browse the library, see what an agent had, or assign anything.

What lands

Tab unhidden for owners/admins on non-system agents, matching the other management tabs; OverflowTabs absorbs it.

stores/skills.js — a domain store (Invariant #6). Worth flagging: the old panel called axios directly with a hand-built auth header, silently bypassing the shared client every other call relies on. Everything now goes through api (Invariant #7).

Library browse with the §21.6 contract: description, automation, user_invocable, declared requires, multi-file count, size, and the git tree SHA as version. Dependencies are shown before assignment, because they're exactly what later becomes a missing_binary:* warning.

Assignment via the existing PUT .../skills with bulk save, plus dirty/reset so a half-made selection is recoverable.

Honest injection status — the load-bearing part. #183 reports injected | unchanged | fallback | failed with named warnings. The panel renders the verdict per skill and translates tokens into what they mean for this agent (missing_binary:jq → "jq is not installed in this agent — the skill may not run"). fallback renders as "partial" in amber, never a green tick — an explicit AC. Injection results are held separately from assignment in the store precisely so durable assignment state can't be painted with a stale success.

Manual sync (force=True repair action) with in-flight state; a 409 from SkillInjectionBusy is reported as "already running", not a generic failure.

No dead empty states — the store computes one discriminator (library_unconfigured / library_empty / none_assigned) so the panel can't invent a fourth. Unconfigured routes an admin to Settings and tells a non-admin to ask one.

Stopped agent renders persisted assignment with Sync disabled and the reason in the tooltip, rather than offering an action that would fail.

Verified against the live instance

Not just rendered — exercised:

library:      3 skills, contract fields rendered
bulk assign:  PUT .../skills → ['haiku', 'word-count'] persisted
agent start:  200
Sync now:     {"haiku": {status: injected, files_written: 2},
               "word-count": {status: injected, files_written: 2}}
UI:           version SHAs + `synced` badges + "Last sync 29/07/2026, 18:06:40"

The stopped-agent path was confirmed first (Sync disabled with tooltip), then the running path after starting the agent.

Not in scope

Exposure curation (#178) — what an agent advertises outward — is deliberately untouched, per the issue. The Settings library panel already reports sync status/last-synced/skill count, so it needed no change; say the word if you'd like it extended further.

dolho added 2 commits July 29, 2026 18:07
…ab (ent#235)

The skills machinery shipped across three planes (#182 distribute/place/expose,
#183 package injection with a per-skill result contract) and nothing rendered
any of it. The Agent Detail Skills tab was excluded from `visibleTabs` per
requirements §22.2 ("component preserved for potential admin-only access"),
assignment was REST/MCP-only (§21.3), and #183's statuses and named warnings had
no consumer at all. A user could not browse the library, see what an agent had,
or assign anything.

What lands:

* **Tab unhidden** for owners/admins on non-system agents, matching the other
  management tabs. `OverflowTabs` absorbs it.

* **`stores/skills.js`** — a domain store (Invariant #6). The old panel called
  `axios` directly with a hand-built auth header, silently bypassing the shared
  client every other call relies on; everything now goes through `api`
  (Invariant #7).

* **Library browse** with the §21.6 contract surfaced: description, automation,
  `user_invocable`, declared `requires` (binaries/packages/env), multi-file file
  count, size, and the git tree SHA as version. Dependencies are shown BEFORE
  assignment, because they are what later becomes a `missing_binary:*` warning.

* **Assignment** with bulk save through the existing `PUT .../skills`, plus a
  dirty/reset affordance so a half-made selection is recoverable.

* **Honest injection status.** This is the load-bearing part. #183 reports
  `injected | unchanged | fallback | failed` with named warnings; the panel
  renders the verdict per skill and translates the tokens into what they mean
  for THIS agent ("`jq` is not installed in this agent — the skill may not
  run"). `fallback` renders as "partial" in amber, never a green tick — an
  explicit AC. Injection results are kept separate from assignment in the store
  precisely so a durable assignment cannot be painted with a stale success.

* **Manual sync** (`force=True` repair action) with in-flight state, and a 409
  from `SkillInjectionBusy` reported as "already running" rather than a generic
  failure.

* **No dead empty states** — the store computes one discriminator
  (`library_unconfigured` / `library_empty` / `none_assigned`) so the panel
  cannot invent a fourth. Unconfigured routes an admin to Settings and tells a
  non-admin to ask one.

* **Stopped agent** renders persisted assignment state with Sync disabled and
  the reason in the tooltip, rather than offering an action that would fail.

Verified against the live instance: tab appears, 3-skill library renders with
contract fields, bulk assign persists, agent started, "Sync now" returns
`{haiku: injected/2 files, word-count: injected/2 files}` and the badges +
last-sync line render from that response.

Gating confirmed OSS-core with the issue author before building: every file here
is already public, the endpoints are ungated, and the paid piece (skill_runner,
ent#139) plus exposure curation (#178) are both explicitly out of scope.

Related to trinity-enterprise#235
…t#235 review)

Self-review of #1877 found two defects, both in the "no dead empty states" AC
this panel exists to satisfy.

1) The "Configure the library" CTA linked to `/settings?tab=skills`. There is no
   such tab — the Skills Library config lives under Settings → **agents**
   (`Settings.vue`, `v-if="activeTab === 'agents'"`). So the one call-to-action
   offered to an admin staring at an unconfigured library went nowhere. I also
   asserted in the PR body that the Settings panel already reported sync status
   / last-synced / skill count without checking; it does report all three — but
   I had the tab wrong, which is what checking would have caught.

2) `api.get('/api/skills/library').catch(() => ({ data: [] }))` swallowed every
   error, not just the unconfigured case: a 500, a timeout or an auth failure
   all rendered as "the library is configured but has no skills yet" — a
   confident, wrong empty state that points the operator at the wrong problem.
   The list is now fetched only when `status.configured` is true, so the known
   empty state comes from the status read and any other failure surfaces as one.

Related to trinity-enterprise#235
@dolho

dolho commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

/review — self-review

Branch: feat/235-skills-uidev · 3 files, +386/−282 (diffed against the merge-base, not the base tip)

Scope: CLEAN. One commit, one issue. All 9 ACs traced; the two that were not actually met are below. (A src/backend/enterprise line appears in a naive git diff dev — that is working-tree noise from a sibling worktree, not in the commit. Verified with git diff <merge-base> HEAD.)


Critical — both fixed in 7ac03171

[C1] The "Configure the library" CTA was a dead link (Confidence 10/10)

<router-link v-if="isAdmin" to="/settings?tab=skills">Configure the library</router-link>

There is no skills tab. Settings has general · access · integrations · mcp-keys · agent-permissions · security · sso · agents · retention · activation, and the Skills Library config lives under agents (Settings.vue, v-if="activeTab === 'agents'").

So the single call-to-action offered to an admin looking at an unconfigured library went nowhere — inside the "no dead empty states" AC this panel was built to satisfy. Now points at agents.

Related: I asserted in the PR body that the Settings panel already reported sync status / last-synced / skill count, without checking. It does report all three (skill_count, commit_sha, last_sync) — but I had the tab wrong, and checking is what would have caught that.

[C2] Every library error rendered as "empty library" (Confidence 9/10)

api.get('/api/skills/library').catch(() => ({ data: [] })),

The intent was to tolerate an unconfigured library. The effect was to swallow all errors — a 500, a timeout, an auth failure — and render "The library is configured but has no skills yet": a confident, wrong empty state pointing the operator at the wrong problem, which is the failure mode this panel is supposed to remove.

Fixed by fetching the list only when status.configured is true. The known empty state now comes from the status read; anything else surfaces as a real error.


Clean

XSS — no v-html; skill names/descriptions are {{ }}-interpolated (they originate from an admin-controlled git library, but are not trusted into markup either way). Invariant #6/#7 — new domain store, all HTTP via the shared api client; this PR removes the old panel's direct axios + hand-built auth header. Auth — tab gated on can_share && !isSystem, matching the sibling management tabs; no new endpoint. Stale-state — injection results are held separately from assignment, so a durable assignment can't inherit a stale success (verified in the store shape, and the "not synced from this screen yet" line covers the no-result case). Empty-state completeness — one discriminator in the store, so the panel cannot invent a fourth branch.

Informational — not fixed

[I1] SkillMeta drops a passed class (6/10) — the inline functional component renders its own class and doesn't merge fall-through attrs, so class="mt-1" at one call site is ignored. Cosmetic spacing only; verified the component itself renders correctly on the live instance (213 B / 277 B).

[I2] watch(() => store.assigned, resetDraft, {deep:true}) (5/10) — if assigned ever changed while a user had unsaved ticks, the draft would be silently reset. No polling or WS feed touches this store today, so it can't fire; worth remembering if one is added.


Verification

Re-ran the live probe after the fixes: tab present, 3-skill library renders, save button present, and the earlier end-to-end still holds — bulk assign → agent start → Sync now{haiku: injected/2 files, word-count: injected/2 files} with badges, version SHAs and last-sync line rendering from that response.

Critical: 2 (fixed) · Informational: 2 · Scope: clean

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills management surface (ent#235): frontend-only (SkillsPanel + skills store + AgentDetail tab wiring), no backend change. Live-probe verification in PR comments covers browse/assign/sync end-to-end. Security greps clean.

@vybe
vybe merged commit 8a768b6 into dev Jul 29, 2026
20 checks passed
@obasilakis

Copy link
Copy Markdown
Contributor

Heads-up from ent#237 (multi-source skills library) so we don't collide, @dolhoI have deliberately not touched SkillsPanel.vue, since you're rewriting it here.

Two things that are now available for the rebuild whenever it suits you (backend already shipped on feature/ent-237-multi-source-skills):

GET /api/skills/library entries carry two new fields:

{
  "name": "pdf-export",
  "source_name": "Acme internal skills",   // which repo it came from
  "shadowed_by": [                          // lower-precedence repos ALSO shipping this name
    { "source_id": "src_…", "source_name": "Trinity Community Skills" }
  ]
}
  • source_name is a name only, never a URL — deliberate, because that endpoint is reachable by agent-scoped keys and a private repo's URL is itself sensitive. Source URLs live on the admin-only GET /api/skills/sources.
  • shadowed_by non-empty means another repo also ships that skill name and its copy is unreachable. ent#237 AC#4 requires the conflict be visible — "conflict surfaced in the UI, never a silent overwrite" — so a badge/tooltip on the row would be the natural home for it. Zero urgency; it's additive and the field is simply ignored until something renders it.

Also FYI: I've split the admin side into components/SkillSourcesPanel.vue + stores/skillSources.js rather than adding to stores/skills.js, specifically so this PR's file isn't in my diff. Different domain too — yours is per-agent assignment, mine is which repos the platform syncs from.

No action needed to merge this PR; I'll rebase ent#237 onto it.

vybe pushed a commit that referenced this pull request Jul 31, 2026
…endpoint rot sweep (ent#263)

- requirements/core-agent.md: new §4.5 Library Page — unified /library surface
  (agent templates + fleet skills browse), query+hash-preserving /templates
  redirect, stacked sections, per-kind empty states, the AC#4 page-identity
  naming rule; fleet assignment visibility named as Not Built
- requirements/skills.md (surgical — §21.3/§22.2/new §22.3 only, avoiding PR
  #1901's §21.1/§21.5 hunks): §21.3 stale 'Skills tab is hidden' note corrected
  (visible since ent#235/PR #1877); §22.2 rewritten as visible/rebuilt; new
  §22.3 Library Page fleet skills browse — browse-only over the existing
  /api/skills/library reads, own skillsLibrary store + the KeepAlive rationale,
  admin-only URL/Sync, #1901 forward-compat, assignment read = Not Built
- architecture.md: 'Top-nav IA — Library (ent#263)' paragraph beside the #1109
  Operations one; stale 'Templates (4 endpoints)' table corrected to the 2 real
  routes (POST /refresh AND GET /env-template both verified absent)
- feature-flows: templates-page.md git-mv'd to library-page.md + full rewrite
  (the old file was deeply stale — AgentSubNav, dead endpoints); index row +
  platform-settings.md Related-Flows link repointed
- template-processing.md + CREDENTIAL_MANAGEMENT.md: dead env-template
  endpoint references removed/replaced (same rot class as the architecture
  table); Templates.vue references repointed at Library.vue
- user docs (creating-agents.md, faq/agents.md): Templates page → Library
  (+ redirect note)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
obasilakis added a commit that referenced this pull request Jul 31, 2026
Replaces the single URL+branch form with `components/SkillSourcesPanel.vue`
backed by `stores/skillSources.js`, and strips the now-dead single-repo state
and methods out of Settings.vue (~130 lines).

Extracted to a component rather than grown inline: Settings.vue was already
3800+ lines, and per-source rows, an add form, and error surfacing would have
added meaningfully to that.

The store is a SEPARATE domain from `stores/skills.js` on purpose. That store
owns per-agent skill assignment (an owner surface); this one owns which
repositories the platform syncs from (an admin surface, and the grant action of
requirements §21.1.2). They share no state — and keeping them apart also avoids
colliding with the Skills-tab rebuild in ent#235 / PR #1877, which is rewriting
stores/skills.js right now.

UI decisions that carry the design rather than decorate it:
* the list renders in backend RESOLUTION order and the first row is badged
  "wins conflicts". The store never re-sorts — ordering is a backend contract,
  and a client-side sort would silently misreport which source actually wins
* `ref_type` shows as "pinned" vs "branch" with a tooltip explaining that a
  pinned tag which moves is REFUSED. That is the supply-chain posture (§21.1.2),
  so it belongs on the row, not buried in an edit form
* the add form warns when to pin: "when you don't fully control who can merge"
* backend errors surface VERBATIM. A refused moved tag names the tag and says to
  point at a new one; a generic "sync failed" would throw exactly that away
* the shadowed-skills count gets its own banner — non-zero means someone is
  running a different source's version of a skill than intended (AC#4)
* Remove says explicitly that agents keep installed skills and assignments are
  not removed, so it can't read as "strip these skills from my agents"
* a failed fetch leaves the list untouched rather than clearing it — blanking
  would read as "no sources configured", a different and alarming claim
* the add form stays filled on failure so a bad URL is corrected, not retyped

Verified with a real `vite build`.

NOT included: the per-skill source badge + shadow warning in SkillsPanel.vue.
That file is being rewritten in PR #1877 (dolho), so editing it here would hand
him a conflict. The backend already exposes `source_name`/`shadowed_by` on
GET /api/skills/library, so it is a small additive change once #1877 lands.

Refs Abilityai/trinity-enterprise#237
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