Skip to content

feat(workspace): chat tabs hold still while you are there; the Inbox sorts Newest first with an Urgency switch (ent#856) - #3683

Merged
vybe merged 10 commits into
devfrom
feature/856-recency-holds-still
Oct 13, 2026
Merged

vybe merged 10 commits into
devfrom
feature/856-recency-holds-still

Conversation

@vybe

@vybe vybe commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Closes abilityai/trinity-enterprise#856. One of the freeze-week chains (chain-freeze-1013). Draft until the operator checks it by hand on rc4.

What

A — chat tabs hold still. Tabs are sorted when you arrive at an agent and then stay put while you're there.

  • One watcher on the stage agent is the only writer of the tab order.
  • It snapshots the agent's chats on arrival, archived ones included.
    • A reply, a send or a poll never re-sorts the strip.
    • Chats born mid-visit go right after Main, newest-created first.
    • The tab strip, the ⌥⇧↑/↓ chat-switch keys and close-to-neighbour all read that one order.
  • Coming back from a room (by sidebar or by URL) or from the Inbox re-sorts once.
  • Landing (operator ruling 2026-10-13): unread answer first, then the chat last open in this session (kept in memory per tab, forgotten on reload), then the fresh-session arms of ent#784.
    • The unread arm was not on dev, so this builds it.
    • Behaviour change: clicking the agent's row, or pressing Back from one of its chats, returns to that chat instead of a new one.

B — Inbox sort.

  • Action opens Newest first by default, with ties broken on id. A Urgency switch gives today's order unchanged.
  • The sort is in the URL as ?sort=. An unknown value means Newest, and the key is kept out of the stage keys.
  • The choice is remembered per viewer, with storage reads in try/catch. Changing it starts one new stable visit.
  • The control appears on Action only, in the reserved list-head line.
  • The rail's asks list follows the default, which is passed explicitly.

Rulings carried (orchestrator, on the operator's behalf, 2026-10-13)

Review + security

/review (Fable, report-only): MERGEABLE AFTER FIXES, with no critical findings. Both items are fixed:

  • I1 in 8e3491e94: the Inbox skeleton drew the old 36px head line, so the list jumped 8px on first paint. The drift spec now compares the head line too.
  • I2 in 7ee0c2e8a: one replace clears a stray ?from= and ?sort= together.

/cso --diff: no findings.

Tests

  • Full npm run test:unit, including the raw-colour, loading-gate and source-text ratchets: 357 files, 5801 tests passed. This ran at --maxWorkers=2; the default worker count ran out of memory on the engineer.
  • Portal and workspace set after the fixes: 154 files, 2664 tests passed.
  • Mutation checks: each of these was reverted, turned its pins red, and was restored:
    • order in cycleChat
    • the room-URL check
    • retaking on every poll
    • the rail sort
    • the sort in visitKey

Before merge

  • Hand check by the operator on rc4, in both themes:
    • the strip holds still when a reply lands;
    • Back and the agent row return to the chat you left;
    • the Inbox head line is now 44px, up from about 36px. The design contract's ghost select is 44px and has no smaller size. The box is identical on every tab and state, so nothing shifts, but the list starts about 8px lower;
    • the sort select at phone width.
  • Flip the §5.43 and §5.40 status lines on merge.

Notes

🤖 Generated with Claude Code

Trinity Agent (trinity) and others added 10 commits October 13, 2026 16:00
… (ent#856)

tabOrderFrom takes the arrival snapshot (Main first, recency, archived rows
ranked not pruned); agentChatTabs({order}) draws the strip in that frozen
order with born chats after Main by created_at; agentLanding gains arm 0
(most recent unread chat, via agentUnreadLanding) ahead of the last-open
arm; forgetLastOpen drops a closed chat from the session memory.
Requirement §5.43 added first. Shell wiring follows (A2).

Refs abilityai/trinity-enterprise#856

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…last-open (ent#856)

One watcher on the derived stageAgent (+ threadListTrusted) is the only
writer of the arrival snapshot tabOrder: take on a new agent, clear off
stage, retake once when an untaken (cold-link) snapshot becomes trusted.
The strip, cycleChat and closeChat's close-next all read the same order.
The /a/:name route watcher now passes lastOpen (landing arm 1) and
replaces the history entry it lands from, so Back does not loop.
forgetLastOpen runs on close and on a thread-missing 404.

Existing pins updated to the 2026-10-13 ruling: a route-driven landing
from a thread of the same agent returns to that thread (was: fresh), and
the openThread/landOnAgent signature pins carry the replace option.
Mutation: reverting openThread(row, { replace }) turns the Remount spec red.

Refs abilityai/trinity-enterprise#856

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… off stage (ent#856)

portalChatCycle: a reply that re-sorts the thread list mid-walk causes no
skip and no repeat; a snapshot for another agent is ignored.
portalAgentLandingStage: the snapshot is retaken on agent -> room -> agent
(sidebar door and by URL) and agent -> Inbox -> agent, held on a same-agent
click, and a cold link is untaken until trusted, retaken once, then held
through a later poll and a failed-then-recovered poll.

The room-by-URL case found a gap in A2: Back/forward or a link to
/workspace/r/:id never writes activeRoomId, so stageAgent kept the agent
on stage. stageAgent now also reads activeRoomIdFromRoute.
Mutations: dropping order from cycleChat, dropping the route-room term,
and retaking on every trusted flip each turn one of these red.

Refs abilityai/trinity-enterprise#856

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing note (ent#856)

workspace-chat-tabs-and-titles.md gains the section: the one stage-key
writer, the comparator, the unpruned snapshot, landing arms 0/1/fresh,
replace-on-route-landing, and the test map. §5.43 records the intended
arm-1 behaviour change (Back / the agent row from one of its chats returns
to the chat left open in this session — the 2026-10-13 landing ruling) and
that a room reached by URL is off stage.

Refs abilityai/trinity-enterprise#856

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…itch, T1/T2/T3/T5 recorded (ent#856)

Requirements first (RoE 1): the Action row of the §5.40 table reads Newest
by default with Urgency on the control; the new 'Action's sort' block
carries AC B1-B7 and the orchestrator rulings — T1 Action only, T2 the
operator queue left as is, T3 the rail follows the default explicitly,
T5 the three inconsistencies recorded (iii is a public bug for handoff).

Refs abilityai/trinity-enterprise#856

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…equest (pure layer) (ent#856)

portalInbox.js: INBOX_SORTS / INBOX_SORT_DEFAULT ('newest'),
normalizeInboxSort (allowlist), askRecencyCompare (created_at desc, id
desc — byAtDesc's rule), inboxSortFor(tab, query, stored) (URL, then the
viewer's memory, then Newest; null off Action), inboxSortKey /
loadInboxSort / saveInboxSort (identity-bucketed, try/catch, fail-open),
and actionItems(openAsks, now, sort). The urgency order (§3g C1) is
unchanged and now on the switch.
portalRailAsks.js: railAskItems passes INBOX_SORT_DEFAULT explicitly (T3).

RED first: portalInboxSort.spec.js (new) and the rail's T3 case failed on
the missing API and the urgency order. Existing pins moved to the ruling:
portalInboxActionSort passes 'urgency'; portalInbox.spec.js's default case
reads newest-first; the deep-linked-ask hold pin selects the older ask so
it still proves the second slot holds. Mutation: the rail passing
'urgency' turns two portalRailAsks cases red.

Refs abilityai/trinity-enterprise#856

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r memory (ent#856)

PortalInbox.vue: sort = inboxSortFor(tab, ?sort=, remembered) — the URL,
then the viewer's memory (bucketed by resolveLayoutIdentity), then Newest;
null off Action. actionItems takes it; visitKey carries it, so a change is
a new visit (rows re-sort on the click, then hold still). onSort writes
both the URL (replace) and the memory. A stray or unknown ?sort= leaves
the URL, like a stray ?from=. The control is BaseSelect variant="ghost"
("Sort asks": Newest / Urgency) in the list head line, Action only (T1).

PortalInboxList.vue: the head line is one fixed 44px box (the ghost
select's height) with a #head-end slot; the loading reserve draws the
same box, so neither a tab switch nor landing data moves the list. The
line grows from ~36px to 44px once, on every tab and state alike.

Mount pins (RED first, 7 failing): Newest default + the control's name and
options; switching writes ?sort= by replace and the memory; URL > memory >
default; another viewer does not inherit; unknown and off-Action ?sort=
stripped; a sort change is a new visit; priority + expiry marks in both
orders; one head-line box across states. Mutation: dropping sort from
visitKey turns two red.

Refs abilityai/trinity-enterprise#856

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
workspace-inbox.md: Action's order line reads Newest by default with
Urgency on the switch; a new section maps sort -> actionItems -> visitKey
-> onSort (URL replace + identity-keyed memory), the stray-key watcher,
the 44px head-line box, the rail's explicit default (T3) and the T2/T5
records; the test table gains the ent#856 specs.

Full npm run test:unit (--maxWorkers=2; the default worker count was
OOM-killed): 357 files, 5801 tests passed, ratchets included.

Refs abilityai/trinity-enterprise#856

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ent#856)

The shell skeleton (drawn by Portal.vue until the stage has a verdict)
still drew the old ~36px head line while PortalInbox's reserve and
PortalInboxList's ready head are the 44px h-11 box, so the first-paint
-> Inbox hand-off moved every row down 8px. The drift spec compared only
the header and tab strip; it now compares the head line against the
reserve's classes and the ready head's box too.

Red before the fix: 'the list head line is the Inbox's one 44px box'
(received 'shrink-0 px-4 pt-3 pb-2 ...').

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (ent#856)

Two immediate watchers each called replaceQuery in the same tick over the
same stale route.query, so the second replace put the first's key back
(?tab=unread&from=x&sort=newest ended as ?tab=unread&from=x). One watcher
now returns both stray keys and clears them in a single replace.

Red before the fix: 'a stray ?from= AND a stray ?sort= together both
leave the URL in one replace (review I2)' (received from: relay).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vybe vybe added the ui PR touches the frontend UI — triggers Playwright e2e tests label Oct 13, 2026
@vybe
vybe marked this pull request as ready for review October 13, 2026 22:29

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

PR Validation Report — APPROVE

PR: #3683, feat(workspace): chat tabs hold still while you are there; the Inbox sorts Newest first with an Urgency switch (ent#856)
Branch: feature/856-recency-holds-still → dev · tip 7ee0c2e8a
Files: 26 (+1098/−90) · Lane B (frontend + docs; no auth, credential, CI or image paths; no schema)

Category Status Notes
Commit messages ✅ 10 conventional feat/fix(workspace) commits
Base branch ✅ dev
PR size ✅ 26 files
Issue / auto-promotion ✅ Implements abilityai/trinity-enterprise#856 (P2, type-feature, theme-ui-ux). Cross-tracker, so after merge set status-in-dev on ent#856 by hand.
Requirements ✅ requirements/core-agent.md covers §5.43 / §5.40. Flip their status lines on merge, as the PR body says.
Feature flows ✅ workspace-chat-tabs-and-titles.md and workspace-inbox.md updated
Architecture ➖ no endpoint, schema or topology change
Security §4.1–4.10 ✅ No keys or IPs. The one email is the example.com fixture. No host paths or mode flips. Enterprise-docs grep: 0 hits.
Build / config packaging ➖ frontend only, no new env vars
Structural review ✅ /review was MERGEABLE AFTER FIXES; I1 and I2 were fixed in 8e3491e94 and 7ee0c2e8a. /cso --diff was clean.
Design-system contract ✅ The head line is a fixed h-11 box, drawn the same in the list, the loading reserve and the skeleton. That is the ghost BaseSelect's size. The #head-end slot uses a primitive, not a hand-rolled select. Raw-colour, loading-gate and source-text ratchets are green. The 44px change is deliberate and bound by the contract.
Merge gate (Tier 1) ✅ Every check on the tip passed. regression diff, e2e and journey-smoke are green. dev-ci on dev is success, and there is no open dev-ci red issue.

Execution coverage (§5.4)

Changed symbol Executed by Live consumer Verdict
tabOrderFrom / agentChatTabs(order) portalChatTabsHold.spec.js, portalChatCycle.mount.spec.js Portal.vue tabOrder watcher → PortalConversation → PortalChatTabs, cycleChat, closeChat ✅
stageAgent watcher (one writer) portalAgentLandingStage.mount.spec.js (mounts the shell) Portal.vue ✅
agentUnreadLanding (arm 0) portalAgentLandingUnread.spec.js agentLanding → landOnAgent ✅
route-watcher replace: true + lastOpen portalAgentLandingRemount.mount.spec.js Portal.vue agent-page watcher ✅ Mutation run by me: I removed replace: true and "back to the agent page … returns to that chat (ent#856 arm 1)" went red.
inboxSortFor / actionItems(sort) / storage portalInboxSort.spec.js, portalInboxActionSort.spec.js PortalInbox.vue actionBase ✅
sort control, ?sort=, stray-key replace, visitKey portalInbox.mount.spec.js (mounted; its toContain reads are rendered DOM and class strings, not source text) PortalInbox.vue ✅
railAskItems default order portalRailAsks.spec.js rail Asks tab ✅

Local run in the PR worktree (--maxWorkers=2): tests/unit/portal*, workspaceNewChat and the three ratchets. 133 files, 2230 tests passed.

Warnings

  • None blocking.

Suggestions (optional)

  • forgetLastOpen(lastOpen, sid) in closeChat is not pinned. With it removed, all 77 tests in the landing, cycle and hold specs stay green. That is expected, because agentLanding already refuses an archived chat. It is the belt-and-braces the docblock says it is, so no test is needed, but a one-line unit test on forgetLastOpen would stop it from rotting silently.
  • The deleted tests (… still lands fresh (T4), "longest-waiting first among equals") are the behaviour changes the operator's 2026-10-13 ruling asks for, and each is replaced by a test for the new behaviour. They do not loosen any guard.

Recommendation

APPROVE. The code, tests and contract are in order. The PR is still draft until the operator checks it by hand on rc4: the strip holds still, Back returns to the last chat, the 44px head line in both themes, and the sort select at phone width. Merge once that check is done and the PR is marked ready. After merging, confirm dev-ci success on the merged SHA and set ent#856 to status-in-dev.

/validate-pr · trinity agent · chain-freeze-1013 / chain-freeze-1013-856-validate

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

merge-train: batch validated on train/20261013-2303 (#3685)

@vybe
vybe merged commit aca0a01 into dev Oct 13, 2026
27 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ui PR touches the frontend UI — triggers Playwright e2e tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants