Skip to content

feat(workspace): give each agent a page (ent#360) - #2147

Merged
vybe merged 2 commits into
devfrom
feature/ent360-workspace-agent-page
Aug 13, 2026
Merged

feat(workspace): give each agent a page (ent#360)#2147
vybe merged 2 commits into
devfrom
feature/ent360-workspace-agent-page

Conversation

@dolho

@dolho dolho commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Issue: ent#360. The last unstarted item on the release-required Workspace list.

An agent had no home. A roster row emitted new-chat-with-agent, so there was nowhere to see what an agent had been doing, nowhere for it to ask you something while no chat was open, and nowhere to show what it can do.

Header · stats strip · five tabs (Overview · Reports · Files · What it can do · Activity). Overview leads with what the agent is waiting on you for, then recent work, then your chats with it.

The half worth reviewing

The page reports; it does not configure — no schedules, no skill editing, no logs, no costs, and model/plan are not shown at all. And the same page serves an external portal-token client as well as a platform user, which makes this a security surface rather than a layout exercise.

So every exclusion is a projection in the service, before the payload exists — not a filter in the template. A template filter is correct right up until someone adds a column to a list view, and nothing fails when they do. A field that never leaves the service cannot be surfaced by a later edit.

Surface What the accessor also returns Why it must not ship
recent_work message, cost, model_used, source_user_email message is another user's prompt; the other two are excluded by AC #7
asks context, and alert items context is free-form agent JSON and a known credential-leak surface (canary G-04 exists because secrets turn up there). alerts are ops telemetry — sync-failing, git-bloat, breaker — not an agent asking a person anything
report detail any report in the install Report ids are global; the roster gate proves only that the caller may reach this agent. A foreign id answers with the same 404 as a missing one, so it is not an existence oracle either

Two AC #3 metrics, opposite outcomes

First-try rate ships and is real: successes with retry_count 0. Deliberately distinct from the success rate, which counts a retried-then-succeeded execution as a success — the right answer to "does it get there in the end", the wrong one to "does it get there first time". NULL retry_count (pre-#678 rows) reads as zero; no terminal rows ⇒ null, not 0.0, because 0% reads as "it fails every time".

Rating tally does not ship. There is no rating, thumbs or feedback mechanism anywhere in Trinity — no table, no column, no endpoint. It has no data source, so I omitted it rather than inventing one; a number a user reads as "how well is this agent doing" has to come from something real. This is the one AC bullet not met, and it is recorded as such in the requirement rather than quietly dropped.

Degradation (AC #6)

Everything is DB-sourced, so a stopped agent renders degraded rather than empty, and a failing data source degrades only its own section — a page that 500s because the operator queue is unhappy is worse than one without its asks. Health reports unknown, never unhealthy, when nothing has ever checked the agent: monitoring is default-OFF (#1121), so on many installs that is every agent and "unhealthy" would be a lie about the whole fleet.

Reuse, as the Technical Notes ask

get_agent_analytics (#1107) via the DB accessor, not over HTTP — the platform endpoint is JWT-gated and a portal-token client cannot call it. Reports and files reuse their existing surfaces. "What it can do" projects the roster briefing (#138/ent#380) rather than building the competing mechanism ent#178 will own.

Supersedes

ent#359 made a roster row with unread open the unread chat, because a badge reading "2 replies" next to a control that opened a blank chat was a contradiction. The page resolves it properly — the row opens the page, the badge still shows, and Overview lists the chats with their counts. "An agent is a destination" is finally true.

Verification

Live against the running instance: header with real health + last-active, 37 executions, 89% completed, 33/37 first try, recent_work carrying exactly the six safe keys, and 404 / 422 / 401 on the roster, window and auth gates.

14 new backend tests (projections, cross-agent report isolation, 404 uniformity, degradation, first-try arithmetic); 442 passed across the portal+session suites; 169 frontend tests; build clean.

Stacking

Branched off #2139 (ent#359), so this diff carries that work until it merges. #2139 first.

Docs: requirements §5.11, flow docs/memory/feature-flows/workspace-agent-page.md.

Related to abilityai/trinity-enterprise#360

dolho and others added 2 commits August 13, 2026 11:24
An agent had no home. A roster row emitted `new-chat-with-agent`, so there was
nowhere to see what an agent had been doing, nowhere for it to ask you something
while no chat was open, and nowhere to show what it can do. Clicking an agent
now opens its page; Start a chat is an explicit button there.

Header (avatar, name, description, health, last active), a stats strip
(activity chart, tasks in window, completed rate, first-try rate), and five
tabs: Overview · Reports · Files · What it can do · Activity. Overview leads
with what the agent is waiting on you for, then recent work, then your chats
with it.

**The interesting half is what it does NOT carry, and where that is enforced.**
The page reports, it does not configure — no schedules, no skill editing, no
logs, no costs, and model/plan are not shown at all. And the same page serves an
external portal-token client as well as a platform user, which makes it a
security surface rather than a layout exercise.

So every exclusion is a PROJECTION in the service, before the payload exists,
not a filter in the template. A template filter is correct until somebody adds a
column to a list view, and nothing fails when they do; a field that never leaves
the service cannot be surfaced by a later edit. Three that matter:

  * `recent_work` drops `message` (another user's prompt), `cost` and
    `model_used` (excluded by AC #7), and `source_user_email`.
  * `asks` admits only agent-authored approval/question items — never platform
    `alert`s, which are ops telemetry (sync-failing, git-bloat, breaker) rather
    than an agent asking a person anything — and never their `context`, which is
    free-form agent JSON and a known credential-leak surface (canary G-04).
  * report reads are agent-scoped. Report ids are global and the roster gate
    proves only that the caller may reach THIS agent, so without the ownership
    check the page would read every report in the install. A foreign id answers
    with the same 404 as a missing one.

Everything is DB-sourced, so a stopped agent renders degraded rather than empty
and a failing data source degrades only its own section. Health reports
`unknown` rather than `unhealthy` when nothing has checked the agent —
monitoring is default-OFF, so on many installs that is every agent.

Reuses the analytics accessor the issue names, via the DB layer rather than over
HTTP (the platform endpoint is JWT-gated and a portal client cannot call it),
and "what it can do" projects the roster briefing rather than building the
competing mechanism ent#178 will own.

Two AC #3 metrics, opposite outcomes. **First-try rate ships** and is real:
successes with `retry_count` 0, deliberately distinct from the success rate,
which counts a retried-then-succeeded execution as a success. **Rating tally
does not**: there is no rating, thumbs or feedback mechanism anywhere in Trinity
— no table, no column, no endpoint — so it has no data source and was omitted
rather than invented. Recorded in the requirement as the one bullet not met.

Supersedes ent#359's interim roster-click behaviour (a row with unread opened
the unread chat). Nothing is lost: the badge still shows on the row, and the
page's Overview lists the chats it belongs to with their counts.

Verified live: 37 executions, 89% completed, 33/37 first try, `recent_work`
carrying exactly the six safe keys, and 404/422/401 on the gates.

Related to Abilityai/trinity-enterprise#360

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… element

F18 asserts the rooms capability is a term in `<PortalRoom>`'s mount condition.
Its mechanism was `indexOf('v-if=', roomAt)` — unscoped, and matching `v-if=`
literally.

ent#360 puts the agent page ahead of the room in the chain, so PortalRoom became
`v-else-if`. That string does not contain `v-if=`, so the search walked straight
past it into the NEXT branch's `v-if` and reported a missing gate that was in
fact present, one line above where it looked.

Now scoped to PortalRoom's own element and accepting either form. Verified by
mutation: deleting `store.multiAgentChatAvailable` from the condition still
fails the test, so the protection is intact rather than relaxed to fit.

Related to Abilityai/trinity-enterprise#360

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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.

Validated via /validate-pr. All 4 required checks green; the stacking prerequisite is satisfied (#2139 merged at 07:47 today). Docs present: requirements §5.11 + docs/memory/feature-flows/workspace-agent-page.md. client_portal/ is COPYed wholesale at docker/backend/Dockerfile:137, so agent_page.py is packaged — no #1033 exposure. No new os.getenv, no secrets, no schema change.

I verified the security claims in the code rather than taking the table at its word, since this payload serves an external portal-token client:

  • _recent_work is an allowlist dict of six keys — message, cost, model_used, source_user_email never enter the payload. Correct call: a template filter would silently stop covering a column added to the accessor later.
  • _asks drops context (the canary G-04 credential surface) and gates on ASK_TYPES = ("approval", "question"), so alert ops telemetry — sync-failing, git-bloat, breaker — cannot reach a client.
  • report_detail scopes on row["agent_name"] != agent_name → None → 404, identical to a missing id. Report ids being global makes that check load-bearing, and returning the same 404 for both keeps invariant #8 intact.
  • window is validated against WINDOWS with a 422 rather than coerced to a default — right for an axis.

Two judgement calls I agree with: first_try as a metric genuinely distinct from success rate (and null rather than 0.0 on no terminal rows, since 0% reads as "always fails"); and shipping AC #3's rating tally as not met and recorded instead of inventing a data source that does not exist anywhere in Trinity.

Design-system note (non-blocking, and not this PR's debt). The new PortalAgentPage.vue carries 4 non-gray + 79 gray raw palette classes. I scanned the branch against current dev rather than trusting raw-color-baseline.json, and #2147 causes zero regressions on existing files — it adds exactly one new file, whose usage is in line with every portal sibling (PortalConversation 59 gray, PortalRoom 46, PortalSidebar 35). What the scan did surface is that the ratchet is not actually being enforced: raw-color-baseline.json is stale by 35 missing files and 22 grown counts, and no workflow in .github/workflows/ invokes scan-raw-colors.mjs. Worth a separate issue to regenerate the baseline and wire the scanner into CI — right now the contract in CLAUDE.md §9 has no teeth.

Merging. ent#360 is cross-tracker so nothing auto-promotes; I'll set its status manually.

@vybe
vybe merged commit 74a569a into dev Aug 13, 2026
25 checks passed
vybe pushed a commit that referenced this pull request Aug 13, 2026
Four conflicts, all additive — both sides added at the same point, so each
resolves as a union rather than a choice:

  * `core-agent.md` — #2147 (ent#360) and this branch both wrote a `### 5.11`.
    #2147 merged first and keeps the number; ent#361 renumbers to **5.12**.
    No cross-references to either anchor exist elsewhere in docs/ or src/.
  * `portalUtils.js` — #2133's budget constants and ent#361's `mentionedAgents`
    are unrelated exports; both kept.
  * `PortalConversation.vue` — one import line, merged to name all three
    symbols.
  * `portalSidebarIA.spec.js` — two `describe` blocks whose conflict cut through
    both; ent#361's is closed and #2133's follows it.

Verified: 282 frontend tests across 19 files pass, vite build clean, and an
unfiltered marker sweep over the whole tree is empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vybe pushed a commit that referenced this pull request Aug 13, 2026
No conflicts. Merged rather than left alone because this branch's CI last ran
against a `dev` from before #2147, #2148, #2150 and #2151 landed — three of
which touch the same portal files (`client_portal/service.py`,
`PortalConversation.vue`, `Portal.vue`). A green run against the older base is
not evidence the combination works, and it also leaves a stale cancelled pytest
shard on the PR.

Frontend suite on the merge result: 282 tests across 19 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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