SideButton Knowledge Module
SideButton Portal — Capabilities — Knowledge Module
SideButton knowledge module — UI selectors, data model, and page states documenting Portal — Capabilities.
sidebutton install sidebutton PRIVACY CONVENTION: This skill file must describe the PAGE — elements, selectors, layouts, states, and public UI copy (placeholder text, button labels, default headings). It must NOT record tenant-specific data visible to a logged-in user: real ticket keys (use
PROJ-123), real agent hostnames (useagent-hostor{hostname}), real IP addresses (use203.0.113.10), real workflow/automation IDs (use{id}), real user names or emails, real Jira host (use{tenant}.atlassian.net), real registry names (use{registry-name}), or real automation display names. When re-verifying, generalize observed values before writing them into this file.
SOURCE OF THIS FILE (2026-09-03): written from the shipped implementation, with route behaviour confirmed unauthenticated. Everything below marked (source) comes from the repo rather than from a browser pass; re-verify it logged in before raising
confidence.
Portal — Capabilities
What This Is
The account's capability sheet: what can this fleet actually do, in six server-rendered tabs — Roles · Workflows · Playbooks · Skills · Workspace · Agents. It is the hub of the sidebar's Settings level and the home of the per-agent role and effort controls. Three older pages became tabs of it and stay reachable as deep pages (/portal/roles, /portal/workflows, /portal/skills).
Its design rule is data honesty: the page never merges what the portal asked for with what an agent reported, and it never invents an answer it does not have. That is why a delivery field says who stamped it, a declared-but-unrouted workflow verdict says so, and the two workspace-shaped tabs refuse to answer at all under All workspaces.
URL Patterns
| Pattern | Behaviour |
|---|---|
/portal/capabilities | 302 → /portal/capabilities/roles (verified unauthenticated); the query string rides along, so a ?workspace= deep link survives the hop |
/portal/capabilities/{tab} | The page; {tab} ∈ roles · workflows · playbooks · skills · workspace · agents |
/portal/capabilities/{unknown} | 302 → …/roles, the same house style unknown params take elsewhere in the portal (source) |
?workspace={slug} | Scope override for that request; an unknown or archived slug is ignored (source) |
?role={slug} | Filter, honoured on the Workflows and Skills tabs only (source) |
Unauthenticated, every tab 302s to /portal/login (→ Auth0). A nonexistent portal path 404s, which is how the routes above were confirmed to exist.
Page Structure
+--[Sidebar: Settings level]--+--[Main]--------------------------------+
| < Back to fleet | H1: "Capabilities" |
| CAPABILITIES | scope line: workspace or All |
| Capabilities (hub) | +--[tab strip: 6 tabs + counts]----+ |
| Roles (sub) | | Roles | Workflows | Playbooks | |
| Workflows (sub) | | Skills | Workspace | Agents | |
| Playbooks (sub) | +----------------------------------+ |
| Skills (sub) | +--[panel]-------------------------+ |
| Workspace (sub) | | fixed-layout table | |
| Agents (sub) | | row -> click -> detail row | |
| AUTOMATION ... | +----------------------------------+ |
+-----------------------------+----------------------------------------+
The sidebar sub-items and the tab strip render the same list — TABS in src/lib/portal/capabilities-nav.ts — so a sub-item click and a tab click land on one URL and both highlight. The hub item lights on a prefix match; a sub-item lights only on an exact URL match (six siblings share one prefix) (source).
Key Elements
CSS is namespaced cap-* (src/styles/capabilities.css) so nothing collides with the portal design system (source).
| Element | Selector / class | Notes |
|---|---|---|
| Tab strip | .cap-tabs (renders the shared lens-pill markup) | One link per tab, with a count |
| Panel | .cap-panel | Section container; panels stack |
| Table | table.cap-table.cols inside .cap-tw | table-layout: fixed, min-width: 960px, widths from a <colgroup> of percentages |
| Summary row | tr.row (tr.row.open when expanded) | Click toggles; open row gets an accent inset rule |
| Detail row | tr.detail | A colspan row rendered server-side and hidden until the summary row is clicked |
| Status pill | .cap-pill + .ok / .warn / .bad / .mute / .run | Semantic colour only |
| Filter chip | .cap-fchip (.cur = active) | Writes ?role= — a link, not client state |
| Button | .cap-btn + .sm / .primary / .accent / .ghost | Colour rule below |
| Eligibility toggle | .cap-tog (.on when granted) | Roles and Agents tabs |
| Effort select | .cap-select | Agents tab; Workflows tab has its own |
| Busy state | .is-busy on the control | Set while a write is in flight; the control is pointer-events: none |
Button colour rule (source)
| Class | Token | Means |
|---|---|---|
.cap-btn.primary | --sb-primary teal #19D3A6 (portal dark theme; the light-mode token is #15C39A) | Create or write — Register, Edit playbook, Push config |
.cap-btn.accent | --sb-accent orange #FB8C3D (portal dark theme; light mode #F97316) | Job intake — Run a workflow, Run now, Discover |
.cap-btn / .cap-btn.ghost | border / muted foreground | Navigation only |
Orange is exact — nothing but job intake is orange. Teal is not: two teal buttons only navigate (Roles page on a blocked playbook, Create a workspace on the pick-a-workspace state), so treat teal as write or create, unless its label is plainly a page name and confirm before automating a click.
Data Model
The page composes existing reads; no table is its own. Per tab (source):
| Tab | Answers | Writes it carries |
|---|---|---|
| Roles | The account role registry, which agents are eligible for each role and where that set came from, guides · persona · packs behind it, and workflow-tag-only slugs as Unregistered rows | Eligibility toggle → PUT /api/agents/:id/settings; Register → POST /api/roles (admin + trial + pack-consent gated; the toast carries the commit sha or PR link) |
| Workflows | Every agent workflow on the account — ?role= is the only row filter, so a disabled one and one nobody in scope can run stay on the list (dimmed, disabled / no agent); each one's declared verdict tokens and where they route | Effort → /api/pipelines/effort; app → /api/pipelines/agentic-app; enabled → PATCH /api/pipelines/:id (all admin-gated); Run now opens the run modal on that pipeline, Run a workflow opens it unselected → POST /api/jobs; Run now becomes a plain hint when no agent in scope may run its role |
| Playbooks | Every playbook with can run here over the agents in scope, its runs, flow strip and the issue types that start it. A blocked one names why — roles missing or steps that name no role — and a disabled one reads off first | None — the buttons are links to the editor and the library |
| Skills | Pack overview, catalog packs, plugins, workspace skills, the base pack, and every domain of the account pack — labelled linked to {scope} / not linked here, never filtered out; ?role= filters | Discover opens the run modal on the SD coverage workflow with the domain as input — a hint when no agent in scope may run that role, and absent entirely when the account has no SD coverage pipeline |
| Workspace | The scoped workspace's setup and what actually reached its agents | Push config → POST /api/workspaces/:id/apply; results render per agent in a toast |
| Agents | The agents in scope: effective roles and their source, what was delivered, effort and app | Eligibility toggles and the effort override → PUT /api/agents/:id/settings |
Effective roles is the rule the Roles and Agents tabs both paint: enabled_roles when that set is non-empty, else capabilities — and the row states which of the two it read. Both empty renders unrestricted, which is not the same as a grant.
Gating is per endpoint, not per page (source): the three pipeline writes are admin-gated and Register additionally needs the trial + pack-consent gate, while the agent-settings and /apply writes are not admin-gated. A non-admin sees read-only chips where a control would be.
States & Variations
| State | Trigger | What renders |
|---|---|---|
| Scoped to one workspace | The workspace switcher's cookie, or ?workspace={slug} | Every figure answers that workspace; the header names it |
| All workspaces | No workspace scoped | Roles · Workflows · Playbooks · Skills answer account-wide and the header says so |
| Pick a workspace | All workspaces + the Workspace or Agents tab | The switcher's own list of active workspaces as buttons, instead of guessing or showing empty rows. Agents adds the one account-wide number it can state; Workspace shows the list alone |
| Row expanded | Click a tr.row | Its hidden tr.detail shows; one open row per table; no column header moves |
| Role filter active | ?role={slug} on Workflows or Skills | The matching chip is .cur; each tab validates the slug against its own chips and otherwise filters nothing |
| Write in flight | Toggle / select / button clicked | The control takes .is-busy (dimmed, inert) until the response lands |
| Write refused | Server rejects an eligibility write | The control reverts to the state the server printed, not to the optimistic one |
| Unregistered role | A workflow metadata.role tag with no registry row | An Unregistered row with a Register button (teal) |
| Declared but unrouted | A verdict token no playbook step routes | The row's detail labels it not routed (the chip carries the long form as a title) rather than hiding it |
| Delivery field | Any row of the Workspace/Agents delivery table | Labelled stamped by the portal at push or reported by the machine at /health |
| Not admin | Non-admin session | Read-only chips instead of the admin-gated controls |
Common Tasks
1. Grant an agent a role Capabilities → Agents (or Roles) → find the agent/role → click the eligibility toggle. This is the only place the per-agent grant is edited; Settings ▸ Agents no longer carries role chips.
2. Change an agent's effort Capabilities → Agents → the effort select in Effort & app. It writes the agent-settings override; clearing it falls back to the agent default, then the account default — the option text names what it will fall back to.
3. Register a role that only exists as a workflow tag Capabilities → Roles → the Unregistered row → Register. The row does not become runnable by itself: the registry row arrives via the pack commit and the next sync, and the fleet still has to be granted the role (task 1).
4. Run a workflow on the scoped workspace Capabilities → Workflows → Run now on the row (or Run a workflow in the header) → the run modal opens on that pipeline.
5. Push a workspace's config to its agents Capabilities → Workspace → Push config. The per-agent outcome renders in a toast; with no agent assigned the button is a link to the workspace editor instead.
6. Check what actually reached an agent
Capabilities → Workspace or Agents → the delivery table. Read the stamped by label before believing a row: portal-stamped means the portal sent it, machine-reported means the agent said so at /health.
Tips
- The URL is the whole state: tab, scope and filter are all in it, so any view is a shareable link.
- Switching tabs keeps
?workspace=; it keeps?role=only for the two tabs that read it. - Only the current tab's loader runs, so a slow tab is that tab's reads — not the page's.
- The three deep pages (
/portal/roles,/portal/workflows,/portal/skills) each carry a ← Back to Capabilities link and light their tab's sub-item in the sidebar.
Gotchas
/portal/capabilitiesis never a view — it 302s to therolestab, and so does an unknown tab. Assert on the final URL, not the requested one.- The unknown-tab redirect is invisible unauthenticated: auth runs first, so
/portal/capabilities/nope302s to/portal/login. Only a logged-in request shows the tab guard. Skills,RolesandWorkflowsare no longer sidebar items — they are tabs. Automation that clicked those sidebar links must open the tab, or the deep page URL directly.- The per-agent role chips and effort select are gone from Settings ▸ Agents (
/portal/settings?tab=agents— the tab they were always on, a sibling of General; retired with the writes landing here) — a test that toggles a role there has nothing to click; the tab links to Capabilities → Agents instead. - Sub-items light on an exact URL match, the hub on a prefix — asserting "active" with a prefix match will report all six as active.
- Opening a row must not move a column header: the table is fixed-layout with
<colgroup>widths and the detail row is acolspanrow. A drift here is a real regression, not a rendering quirk. no guideon an Unregistered row means "not found", not "absent" — a registered role distinguishes an unreadable base pack (unknown) from a real absence; the unregistered rows do not carry that signal yet.- Portal API is not its own column on the Workspace tab — it is a sub-line inside the MCP cell; only the Agents tab gives it a field of its own.
- Do not read a delivery value without its
stamped bylabel — the page keeps requested and reported apart on purpose, and quoting one as the other is the exact confusion this page was built to end. - A toggle can be refused (the server validates the slug against the account registry): the control reverts to the server's answer, so read the row after a write instead of trusting the click.
- Persona editing is not on this page yet (KAN-55 open as of 2026-09-03): the Roles detail's Write persona is a link to
/portal/roles/{slug}.