Repository navigation
feat(workspace): chat tabs hold still while you are there; the Inbox sorts Newest first with an Urgency switch (ent#856) - #3683
Conversation
… (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>
trinity-ability
left a comment
There was a problem hiding this comment.
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)incloseChatis not pinned. With it removed, all 77 tests in the landing, cycle and hold specs stay green. That is expected, becauseagentLandingalready 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 onforgetLastOpenwould 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
left a comment
There was a problem hiding this comment.
merge-train: batch validated on train/20261013-2303 (#3685)
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.
dev, so this builds it.B — Inbox sort.
?sort=. An unknown value means Newest, and the key is kept out of the stage keys.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: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.7ee0c2e8a: one replace clears a stray?from=and?sort=together./cso --diff: no findings.Tests
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.orderincycleChatvisitKeyBefore merge
Notes
resolveAgentQuerystep was left out deliberately, because the bootstrap door has no memory to pass.🤖 Generated with Claude Code