feat(workspace): chats as tabs, ⌘J New chat, and renameable titles (abilityai/trinity-enterprise#451, abilityai/trinity-enterprise#473) - #2552
Merged
Conversation
…bilityai/trinity-enterprise#451, abilityai/trinity-enterprise#473) The remaining ent#451 slice as ruled 2026-09-06, plus ent#473, in one PR. ent#451: this user's threads with the active agent render as OverflowTabs above the thread (dense + a counted moreLabel → "N more"; a slice of the sidebar's list, never a second fetch; an unsaved new chat is not a tab; the pinned Main chat is #523's first slot). New chat moves into the conversation header with ⌘J / Ctrl+J — plain modifier only, armed on window above bootstrap()'s await, resolving the agent in front of the person. ent#473: PATCH .../agents/{agent}/sessions/{id} and PATCH /api/rooms/{id}, validated by ONE leaf (services/chat_title.py: trim, collapse, an inner line break refused, ≤100) with the same named 400 (invalid_title + reason + a sentence with an example). The thread UPDATE is (agent, client)-scoped; the room is membership-then-person (a member agent talks, it does not rename). enterprise_portal_sessions.title_source (NULL · generated · user; SQLite migration + Alembic 0052, no backfill) and the generated write GUARDED IN THE UPDATE, because generation runs off the reply path and races a rename typed inside the first turn's window. _title_plan earns exactly one more pass after a greeting-shaped opener or a first attempt that never landed (message_count <= 2), never over a person's title. A failing generator warns once per episode and rides GET /api/settings/portal-session-policy → title_generation into the Workspace sessions panel's notice. PortalEditableTitle.vue is the one editor for the sidebar row, the 1:1 header and the room header. Also: search_chats now resolves the same roster set agent_on_roster enforces (owned agents for a platform session) — it read only the shared roster, so an owner searching their own agents' chats always got nothing, which made AC 5 untestable on the operator's door. Verification: tests/unit/test_ent473_chat_titles.py (53) + the portal / rooms / migration neighbours (531 passed), Alembic heads guard, full npm run test:unit (1985), check:tokens, vite build; live on the Docker stack — migration applied on reload, rename endpoints (named 400s, uniform 404s, 200s), tab strip, header rename, sidebar rename with the inline refusal, ⌘J, room rename, search by the new title. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TKZeR6GGoLQaZBo5sBUBu
vybe
approved these changes
Sep 6, 2026
vybe
left a comment
Contributor
There was a problem hiding this comment.
Validated via /validate-pr: docs (requirements §5.21, workspace.md, feature flow + index) present; both migration tracks (SQLite portal_session_title_source + Alembic 0052, one head); security greps clean; backend-unit-test head 14014 passed (+55 over base), frontend build/test:unit/check:tokens green, e2e green; structural pass on the rename/scoping/UPDATE-guard paths reads correct (roster→uniform 404, (agent,client)-scoped UPDATE, person-only room rename, thin #918 broadcast).
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Journey Impact: none — UI plus two endpoints inside the existing Workspace chat surface; no journey in the current deck (J01–J10) covers Workspace chat (the same finding as ent#451 / ent#473).
Fixes abilityai/trinity-enterprise#451
Fixes abilityai/trinity-enterprise#473
Summary
Two rulings from the 2026-09-06 operator session, one PR.
ent#451 — the remaining slice (#2430 shipped the fresh-thread half):
OverflowTabs, never a hand-rolled strip, with two additions to the primitive:denseand a countedmoreLabel("N more"; every existing strip keeps "More"). Most recent first, a slice of the sidebar's own list (no second fetch), repacking on rail/window resize because the primitive re-measures onResizeObserver. An unsaved new chat is not a tab (ruling: it exists once its first message is sent), and the pinned Main tab is bug: claude subprocess stdout inherited by MCP child processes — leaks reader threads, eventually wedges agent-server #523's first slot.windowat mount abovebootstrap()'s await (contract Orphaned Execution Recovery #23); resolves the agent in front of the person (page or conversation), the picker otherwise.ent#473 — rename, and trustworthy generated titles:
PATCH /api/enterprise/client-portal/agents/{agent}/sessions/{id}{title}andPATCH /api/rooms/{room_id}{name}. ONE validator leaf (services/chat_title.py: trim, collapse, control chars dropped, an inner line break refused, non-empty, ≤100) imported by both services, so a thread and a room refuse the same titles with the same named 400 ({code: "invalid_title", reason, message}— rule, fix, example). Thread: roster gate, then the UPDATE itself is (agent, client)-scoped → uniform 404 (Invariant security: implement safe tar extraction with symlink/hardlink validation #8), per-viewer rate limit. Room: membership (uniform 404), then person-only — a member agent talks, it does not rename (403not_a_person, the ent#220 line); thinroom_renamedbroadcast with the id only (feat: agent-reported structured reports via MCP + dashboard display #918).enterprise_portal_sessions.title_source(NULL ·generated·user; SQLiteportal_session_title_source+ Alembic0052, no backfill). The generated write is guarded in the UPDATE (title_source != 'user'): generation runs off the reply path, so a rename typed inside the first turn's 15 s window races the model's guess, and a read-then-write would leave exactly that window._title_plan(row, history):firston an empty title (ent#186, unchanged);retryexactly once, on the exchange after the opener (message_count <= 2), when the first attempt never landed or the opener was greeting-shaped (is_greeting); never over a person's title. The retry feeds this exchange — the first one with a topic in it.no_credential(first hit) orfailing(3 in a row), stays quiet in it, re-arms on recovery, and ridesGET /api/settings/portal-session-policy→title_generationinto the Workspace sessions settings panel as a warning notice with the next action.PortalEditableTitle.vueis the one editor for its three homes (sidebar row · 1:1 header · room header): Enter/blur commit, Esc abandons, client-side mirror of the validator, server refusal rendered verbatim in anInlineError, optimistic list update with revert-and-rethrow.Also (found while verifying AC 5 live):
search_chatsread only the shared roster, so a platform user searching chats with their own agents always got nothing. It now resolves the same setagent_on_rosterenforces (roster_agent_names(email, include_owned)).Changes
services/chat_title.py(new leaf),client_portal/{db,service,router,models}.py,shared_sessions/{db,service,router,models}.py,routers/settings.py,db/{schema,tables,migrations}.py,migrations/versions/0052_portal_session_title_source.pycomponents/OverflowTabs.vue(dense,moreLabel),portal/PortalChatTabs.vue+portal/PortalEditableTitle.vue(new),PortalConversation.vue,PortalChatRow.vue,PortalSidebar.vue,PortalRoom.vue,portalUtils.js,stores/clientPortal.js,views/Portal.vue,settings/PortalSessionPolicyPanel.vuearchitecture/workspace.md,feature-flows/workspace-chat-tabs-and-titles.md+ index,learnings.mdNot in this PR (→ #523)
The pinned Main chat, Reset, the merged agent page, and the sidebar's "a recent-chat row opens the agent page with that chat active" — until then a row opens the thread view, which now carries the agent's tabs. No MCP rename tool: rename is a person's verb on the UI.
Test Plan
pytest tests/unit/test_ent473_chat_titles.py(53) — validator table, greeting shape,_title_plandecision table, the UPDATE guard against a real sqlite, rename scoping, search-by-user-title, named 400 at the router, route registration, room rules (person lands + thin broadcast, workspace client is a person, agent refused, same 400), health episodes (once / threshold / recovery / no key material), settings payload, both migration tracks, search roster scopetests/unit/test_ent79_portal_exposure.py— the second pass end to end throughportal_chat(first→retry→ nothing; nothing after a landed topic title)test_ent451/359/358/443/360/457/525,test_2198/2133/2320/2213, migrations, Alembic parity + revision-id + heads guards, auth-wiring / enumeration / models-centralized / settings-sink / admin-gate static guards — 531 passednpm run test:unit— 1985 passed (incl. newportalChatTabsAndTitles.spec.js);npm run check:tokens;vite build--reload): migration applied (title_sourcepresent,schema_migrationsrow); rename endpoints — multiline/too-long → 400invalid_titlewith the sentence, valid → 200 normalised, unknown id → uniform 404, room empty → 400, room unknown → 404; sidebar rows carry the pencil; tab strip renders with the active tab; header rename → sidebar + tab + header update, server persisted; sidebar rename with an empty draft → inline refusal, row does not open, Esc abandons; ⌘J → fresh chat, no active tab; room row opens the room, header rename persisted; search finds the renamed threadRatchets
Raw-color: only gray + semantic tokens added (
status-warning-*for the notice). Loading-gate ratchet spec green. Alembic: one head (0052), parity guard PASS.🤖 Generated with Claude Code
https://claude.ai/code/session_014TKZeR6GGoLQaZBo5sBUBu