feat(workspace): give each agent a page (ent#360) - #2147
Conversation
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>
2a2a2c1 to
8838621
Compare
vybe
left a comment
There was a problem hiding this comment.
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_workis an allowlist dict of six keys —message,cost,model_used,source_user_emailnever enter the payload. Correct call: a template filter would silently stop covering a column added to the accessor later._asksdropscontext(the canary G-04 credential surface) and gates onASK_TYPES = ("approval", "question"), soalertops telemetry — sync-failing, git-bloat, breaker — cannot reach a client.report_detailscopes onrow["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.windowis validated againstWINDOWSwith 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.
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>
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>
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.
recent_workmessage,cost,model_used,source_user_emailmessageis another user's prompt; the other two are excluded by AC #7askscontext, andalertitemscontextis 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 anythingTwo AC #3 metrics, opposite outcomes
First-try rate ships and is real: successes with
retry_count0. 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". NULLretry_count(pre-#678 rows) reads as zero; no terminal rows ⇒null, not0.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, neverunhealthy, 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:
headerwith real health + last-active, 37 executions, 89% completed, 33/37 first try,recent_workcarrying 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