Skip to content

DO NOT MERGE — merge train: 3054,3137,3101,3132,3133 - #3143

Closed
trinity-ability wants to merge 136 commits into
devfrom
train/20261001-0847
Closed

trinity-ability wants to merge 136 commits into
devfrom
train/20261001-0847

Conversation

@trinity-ability

Copy link
Copy Markdown
Contributor

Integration surface for #3054, #3137, #3101, #3132, #3133. Never merged; members merge individually once green.

🤖 Generated with Claude Code

…PR A0)

The requirement (security.md §26.8), the endpoint note and the agent-page
flow state the new contract before the code: 503 asks_unavailable on a queue
or roster read fault, and a store that keeps the last good list.
…er [] (ent#610 PR A0)

list_asks caught every error and returned [], and _on_roster turned an
unreadable roster into "not on the roster" — both made the Workspace say
"nothing needs you" during an outage (the #2915 class). The list now raises
AsksUnavailable (strict roster mode in list only) and the route answers 503.
A clean off-roster agent is still dropped; answer_ask keeps its uniform 404.

test_ent428's unreadable-roster case pinned the old == [] and is reversed
deliberately.
…so (ent#610 PR A0)

fetchAsks treated every error as absence: it cleared the list and set
asksAvailable=false, blanking every PortalAsks surface and zeroing the badge
on a 5xx. Now only 404/403 are absence; any other failure sets asksFailed,
keeps the list and leaves asksAvailable alone. asksLoaded latches on the first
success and asksLoadedAt feeds a stale banner.

workspaceAsks.spec.js's "clears the list rather than showing stale asks"
pinned the old behaviour and is reversed deliberately (ent#253).
Keeping the last good list on a failed read made it session-scoped state,
but signOut() never cleared it, so on a shared browser the next client's
first failed read showed the previous client's asks and badge. signOut now
resets asks, asksAvailable, asksLoaded, asksFailed and asksLoadedAt. A 404
also resets asksLoaded: a surface that no longer exists has no verdict.

Found by /cso and /review. Mutations (drop the sign-out reset; drop the 404
reset) each turn one spec red.
…ent#610 PR A0)

It passes on the pre-fix code too, since the old list_asks returned [].
The flow doc, registry entry and docstring now say so. The flow doc also
names the sign-out reset.
…(ent#610 PR A0)

The #2915 entry's Files and Tests lines now name the asks router, the
store, and both A0 test files. The A0 bullet also names the sign-out reset.
Found by /validate-pr.
…box (ent#610 PR A)

Written before the code (Rule #1): core-agent.md §5.40, workspace.md's Inbox
section, the report-landing rule in observability.md, the frontend paragraph,
the chat-state and report-publish endpoint rows, the new workspace-inbox flow
and its index row, the sidebar/deliverables/agents-at-the-centre/operating-room
flow deltas, and Inbox variants on journeys J05 and J11.
…(ent#610 PR A)

count_unread_by_session stays the only unread function and gains a second
arm over one _UNREAD_ARRIVALS fragment: a report addressed to the viewer,
stamped to a session the viewer owns, after that chat's cursor or the
account baseline. Plain equality on a lowercased bind, so arm (ii) reads
idx_agent_reports_audience (EXPLAIN pinned). The #557 fixtures gain
agent_reports. Mutations (join, addressee, >=, lower()) each go red.
…essee (ent#610 PR A)

report_service.resolve_report_session keeps the publishing turn's chat only
if the addressee owns it, else stamps the addressee's Main (ensure_main_session)
and touches it with added=0, so a report-only Main is listed and Reset still
reads it as untouched. The router calls it; _resolve_portal_session moves with
it (Invariant #1). A report addressed to X during Y's turn no longer lands in
Y's chat, where nobody's reader showed it.

The completion writer stamps source=completion:done|failed from the same
status that picks its wording — the Inbox's outcome pill reads that, never
the body. ensure_main_session's caller list is corrected. The ent365 and ent457
tests that addressed the moved helper / the old writer signature are
retargeted. Mutations (audience=None, owner check, touch, added, marker) red.
…s read (ent#610 PR A)

portalInbox.js holds every Inbox rule as a pure function: the bootstrap
landing target, the Action/Unread/All item builders (archived chats stay in
Unread, rooms excluded, All windowed to 30 days and bounded to 50 with the
total stated, ended asks for 7 days), item keys and the two counts. The
counts read the same sidebarThreads projection the sidebar sums, so the
pinned row and the agent rows cannot disagree; a seeded property test over
500 fleets pins came == totalUnread == sum of unreadByAgent == sum of the
Unread rows. Mutation: dropping archived chats from Unread goes red.

The store's fetchChatState({previews}) returns {state, previews} (the no-arg
call is unchanged) and markChatReadStrict rethrows so Mark all read can count
its failures. /workspace/inbox is a new route on the same shell;
WORKSPACE_ROOT is unchanged. Badge wording moves from replies to new because
deliverables now count.
…10 PR A)

PortalAsks gains askIds (one ask, drawn in place once it ends) and
testidPrefix over every id it emits, the static ones included; the default
prefix keeps every existing id byte-identical. A mount test puts the Work
tab's instance next to an inbox-ask one for the same asks and asserts the id
sets are disjoint.

PortalConversation carries data-message-id on its message wrappers and honours
a one-shot ?anchor=m:<id>|d:<reportId> through useConversationAnchor: detach
stick-to-bottom, scroll the target into view and outline it, or fall back to
the bottom with a 'further up' notice; the anchor key is stripped either way.
PortalDeliverables emits loaded and marks its cards with data-report-id.
useStickToBottom gains detach(). Mutations (always-prefixed sync badge, a
static root id, a no-op detach) each go red.
… (ent#610 PR A)

GET /chat-state?previews=true adds latest {kind, id, at, excerpt, outcome,
title?, display_hint?} and first_unread_message_id to each chat with
arrivals. The read lives in the new client_portal/chat_previews.py;
db.unread_arrivals_with_latest windows the SAME _UNREAD_ARRIVALS fragment the
count groups, and the counts it produces are passed into
service.get_chat_state(unread=...) so a badge and its preview come from one
statement. service.py gains only that optional parameter.

Previews attach only to roster agents (include_owned for platform users),
the 100 most recent. The excerpt is credential-sanitised, markdown-stripped
and capped at 160 chars; outcome comes only from the platform-written
completion:* source marker, never from the body. PortalChatArrival has no
cost and no execution id. Without the flag the payload is the old shape.

Tests: property n == count, latest iff n > 0, latest.at == max; no cost key
anywhere; off-roster; the marker through the real completion writer; a body
starting **Finished** without the marker gives no outcome. Mutations (roster
filter, unread passthrough, cost, latest order, body parsing, first-unread
order, redaction) each go red.
…g pane (ent#610 PR A)

PortalInbox is the container: header with a ghost Mark all read (strict
per-chat reads under allSettled, the failed count named), Action/Unread/All
tabs with counts, list plus pane, and a phone collapse with Back and Esc that
returns focus to the row. Every tab has its own honest state: a skeleton
until roster, threads and asks have loaded, LoadFailed with no data, a stale
banner over data, and empty copy only after a successful read. Tab and
selection live in ?tab=&item=.

PortalInboxList rows are buttons with a stated total; a read chat and an
ended ask stay in place until the selection changes. PortalInboxPane renders
an ask through PortalAsks (inbox-ask ids), or a chat from its first unread
message within a 50-message window plus its deliverables through
ReportRenderer with the ReportSummary fallback; a failed deliverables read is
LoadFailed, not empty (fetchSessionDeliverablesStrict). No cost or execution
id is rendered. Desktop auto-selects the first row without marking it read.
The store gains asksAbsent so an instance without asks does not skeleton
forever. Open canvas is not built: the shell cannot cheaply know.
…0 PR A)

Portal.vue gains the Inbox stage branch after the rooms and before the
conversation, rendered only on a ready stage, so a roster error or an empty
roster still shows its own copy (the ent#253 class). bootstrap() reads the
landing target before its first await and awaits the replace inside the try,
so bare /workspace never flashes agents[0]'s chat. On the Inbox route the
active agent comes from the selected item and activeAgentName is never
written, so selecting does not mint a Main. threadsLoaded latches on the
first good session read; previews are fetched only on the Inbox route; the
sidebar filter is now sidebarThreadsOf, the one projection both counts read.

The sidebar's head badges move to a pinned PortalInboxRow (brand links to
/workspace/inbox), keeping sidebar-ask-count and adding
sidebar-unread-count, white on a 700 ground. Four source pins changed on
purpose, each with its reason in the spec. e2e: a new @smoke inbox spec;
workspace-rail-reserved opens /workspace?agent=<first agent>; the contrast
ratchet measures /workspace?new=1 against its frozen baseline and holds
/workspace/inbox at zero.
…s loud on a roster outage (ent#610 PR A)

Two route cases the build left unpinned: GET /chat-state?previews=true must
pass the caller's own principal kind to the roster read (a hard-coded kind
went green before), and an unreadable roster must be a 5xx, never a 200
with the previews silently dropped. Each mutation (hard-coded kind, a
swallowing roster read) goes red.
The docs commit was written ahead of the code. Corrections: the landing
replace is the first await; selection lives in ?item= (auto-select on
desktop is never a read); the pinned row is PortalInboxRow; anchors run
through useConversationAnchor; the pane's deliverables read is strict;
excerpts are credential-sanitised and a deliverable's excerpt is its title;
a chat without a preview omits the keys; a roster outage fails the previews
read; Open canvas is deferred. The flow's Files and Tests tables now list
every file.
…t#610 PR A)

On All a read chat stays listed, so holdSelected returned the live row and
the next refresh rebuilt it without a preview: the pane fell back to the
last five messages under the reader and Open in chat lost its m: anchor.
While the selection is unchanged the live row now carries the snapshot it
was opened with (first unread message id and latest); another selection
releases it. Mutation (return the live list unmerged) goes red in the pure
and the mount spec.
…t#610 PR A)

refreshThreads swallowed a failed chat-state read, so on the Inbox (where
the previews ride that read, and a roster outage deliberately fails it) the
list kept stale counts and previews with no signal. A previewsFailed flag
now joins the thread verdict the Inbox renders: before any good read it is
LoadFailed and threadsLoaded does not latch; after one it is the stale
banner, cleared by the next good read. Reset on sign-out. Mutation (drop
the flag from the verdict) goes red.
…e's Main (ent#610 PR A)

/cso finding. POST /agents/{name}/reports is gated by AuthorizedAgent, so
every human the agent is shared with can publish as it. With the D4 stamp,
one sharer could address a report to another person on the roster and mint
and touch THAT person's Main: a badge, an excerpt and a deliverable card in
their Inbox that the agent never produced.

resolve_report_session gains a required allow_main keyword (no default, so
every call site states the gate); the route passes
current_user.agent_name == name. A human publish keeps the addressee-owned
in-flight chat and otherwise stays NULL, as before #610. Mutations: forcing
allow_main=True in the route, and dropping the gate in the service, each go
red.
… (ent#610 PR A)

/review finding. resolve_report_session minted AND touched the addressee's
Main before the report insert; the touch sets last_message_at, which is what
lists a report-only Main in the sidebar, so a failed insert left an empty
Main listed. Resolving now only mints (the id is needed for the column);
the route calls the new report_service.touch_report_session (added=0,
fail-soft with a WARNING) after create_report returns. Mutation: moving the
touch back before the insert goes red.
…d selection is held (ent#610 PR A)

Back and Esc focused the row on a tick before the navigation had shown the
list column again, so in a real browser focus() hit a display:none element
and landed on the body; jsdom ignores the hidden class, so the mount spec
passed. Back now awaits the query replace and a tick, then focuses the row,
the row now in its place when the read chat has left Unread, else the list
column (tabindex=-1). A selection restored from the URL (reload, deep link)
is now held exactly as a clicked one, so an answered ask keeps its row in
place until the selection changes (D8).

Tests record at each focus() call whether the target sat under a hidden
column. Mutations: an un-awaited replace, and no URL hold, each go red.
Verified in Chromium at 375 in both themes.
…nt#610 PR A)

At 375px Back plus Reply in chat and Open in chat left the chat title about
four characters. The header now wraps: the title keeps at least 12rem and the
two actions move to their own line; from sm up they stay on one line.
Measured in Chromium, both themes: title 261px at 375, one line at 1280.
…ts report row (ent#610 PR A)

fetchAgentReport returns the whole report ({id, title, payload, row_meta});
the pane handed that to ReportRenderer, so ReportSummary dumped Id / Agent
name / Report type / Title instead of the content. Found on a live stack.
The pane now unwraps full?.payload exactly as PortalDeliverables does. A
mount test asserts the renderer receives the payload; reverting the unwrap
goes red.
…#610 PR A)

The list states its total on a line above the rows, and that line appeared
only with the rows, so the list moved ~36px down when the reads settled
(measured live by the e2e arm). The line's box is now reserved in the
loading, failed and empty states too. A mount test pins the reserve in each
state; dropping it goes red. The e2e arm now skips without a roster agent,
as arm 3 does: with no roster the Inbox never renders (D9), so there is no
list to measure.

The payload test from the previous commit is in this spec file too.
…ent#610 PR A)

/validate-pr §3.3: the new flow lacked User Story, Entry Points, the
Frontend/Backend Layer split, Side Effects, Error Handling, Security
Considerations, a Testing status and Related Flows. Content is reorganised
under those headings with file:line anchors; the error table and the
security notes (roster scoping, no cost, the agent-only Main stamp, URL
params) state what the code does.
AndriiPasternak31 and others added 21 commits September 30, 2026 22:09
…raws it, no card points at Main for it (ent#610 PR A2, 09-30 ruling item 2)

The amendment's second home: an ask raised by a schedule, loop or gate
(raised_in_turn not the literal true) keeps chat_id = Main as a fact on the
row — the Inbox pane's reply lands there — but renders in no chat: no tile in
Main, no history row. The Inbox's Action tab lists it while it waits and its
All tab is its history.

Item 1's chatTurnAsks already keeps it out of every thread; this pins that
for Main by mount, and closes the one pointer left: askThreadLink offered
"Open the conversation" (to Main) on any card with the link on, so a
background ask sent the reader to a chat where the ask is not. It now links
only an ask a chat turn raised. The link fixtures that model a chat-turn ask
say so (raised_in_turn: true); workspaceAsks gains the background case.

Mutations, each once, restored from a scratch copy:
  M1 askThreadLink accepts a truthy raised_in_turn   -> 1 red
  M2 askThreadLink ignores raised_in_turn            -> 2 red
  M3 chatTurnAsks ignores raised_in_turn (Main)      -> 1 red
FE unit 4380/4380.
…pen in Inbox" line; every asks door goes to the Inbox (ent#610 PR A2, 09-30 ruling item 3)

The ruling's item 2: an agent's asks home is the Inbox filtered to that
agent, on every door, for platform users and clients alike. Sign-off round
5's "Work is the asks' home" is reversed:

- Work draws no ask. While any of the chat's agents waits on you it shows
  ONE line, "N waiting on you · Open in Inbox", to asksHomeRoute(agent) =
  /workspace/inbox?tab=action&from=<agent> (§3g C2's filter, kept); a room
  with asks from two agents links to the whole Action tab. It counts
  portal.openAsks (the list the sidebar and the pinned Inbox row read), so
  it never counts an ended ask.
- The line sits on the Now heading's row, which is always drawn, so it
  arriving or leaving moves nothing (principle 30); an empty Work carries
  it under the empty copy.
- Round 5's "Work skips the pane's ask" is undone: nothing is drawn to skip,
  so PortalWork's excludeAskIds, the shell's inboxOpenAskIds and PortalAsks'
  excludeIds are removed, and the line counts the pane's ask too.
- The two other doors that sent "the asks" to Work — the new-chat briefing's
  asks suggestion (Portal.vue openSuggestionSection) and Info's section link
  (PortalAgentDetails openSection) — push asksHomeRoute instead.

Specs rewritten for the new home, not deleted: #3055's Work-rail case is now
a card read from another chat; the Work pending-only case counts the line;
round 5's exclude pair asserts the list is gone; portalWork's source guard
asserts no PortalAsks. New portalWorkAsksLine.spec.js mounts Work with a
real router (hrefs asserted).

Mutations, each once, restored from a scratch copy:
  M1 always narrow to the first agent        -> 1 red
  M2 asksHomeRoute drops ?from=              -> 2 red
  M3 count portal.asks (ended included)      -> 4 red
  M4 the empty state loses the line          -> 5 red
  M5 the asks suggestion opens Work again    -> 1 red
FE unit 4391/4391.
…eds you" mark — one feed with the Inbox row, pinned (ent#610 PR A2, 09-30 ruling item 4)

The ruling's item 3: agent rows carry a "needs you" mark beside the unread
mark, from the same feed the pinned Inbox row counts. The pill predates the
ruling (#2424, coloured by §3g A3b) and already reads openAsks; what was
missing is the contract on it, now pinned by mount:

- one feed: the rows' marks sum to the Inbox row's number for any mix of
  chat-turn and background asks (both need you; only their homes differ);
- pending only: an answered ask leaves the row mark and the Inbox row at
  once;
- it sits beside the unread mark, ask first;
- it names itself on hover in the Inbox row's words (askBadgeTitle: "N asks
  are waiting on your answer") — the one code change; it had no title.

Mutations, each once, restored from a scratch copy:
  M1 the pill loses its title                  -> 1 red
  M2 the rows count store.asks (ended too)     -> 2 red
  M3 the ask pill moves after the unread pill  -> 1 red
FE unit 4394/4394.
…t the retired box's (ent#610 PR A2, item 1 follow-up)

With testid-prefix "portal-chat-ask", PortalAsks names its root
`portal-chat-asks` — the address of the pinned box item 1 removed. The live
probe's "is anything still pinned above the composer?" query matched every
tile. The mount spec could not see it: shallowMount stubs PortalAsks. The
tile is now `portal-tile-ask` (root `portal-tile-asks`), and the spec pins
the prefix prop.

Live-proved on the seeded stack (light + dark, 1440 + 390): tiles sit by
time among the messages in a non-Main chat and in Main; a schedule ask
draws nothing in finance Main; reading 24 rows up, a tile arriving moves
the read position 0px; following at the bottom, it stays 0px from the
bottom; answered in place, the card stays with "Sent." and is a muted row
on revisit; Work reads "8 waiting on you · Open in Inbox" and lands on
/workspace/inbox?tab=action&from=research-agent.
…the 09-30 ruling rework (ent#610 PR A2)

- workspace-agents-at-the-centre: a chat draws only its chat-turn asks, as
  thread rows by time (portalChatAsks); the pinned box and the "N more
  asks" line are gone; the thread link is for chat-turn asks only.
- workspace-work: Work = Now · Earlier + one "N waiting on you · Open in
  Inbox" line (asksHomeRoute); test rows updated.
- workspace-sidebar-ia: the agent row's ask pill is the ruling's "needs
  you" mark, one feed with the Inbox row.
- architecture/workspace.md and requirements/core-agent.md AC-4: Work
  draws no ask.
Disclosure guard clean. Index unchanged (no new flow; 468 lines, pre-existing).
…queue's retention; a background ask names no chat origin (ent#610 PR A2, round 2)

The 09-30 ruling (amended): an ask a chat turn raised stays in that chat's
history once ended, "subject to the same retention as the queue (trinity#1142)";
a background ask lives in the Inbox only.

- GET /asks?chat_id=<id>: the viewer's asks the platform stamped as raised in a
  turn of that chat (ent#734), pending and ended, cleared kept, NO ended window.
  The Inbox read's 7 days and first page of 200 no longer decide what a chat's
  history shows (review I3, codex C3). SQL prefilter on the stored context
  (LIKE-escaped, so `_`/`%` in an id match only themselves); the projection's
  top-level keys decide, so a look-alike key an agent nests never counts.
- _origin: only a chat TURN (triggered_by=public) verifies a thread — a schedule
  that delivers into the Workspace stamps the portal channel and Main on its run,
  so its ask read "came from Main" with Main's unrelated messages (codex C4, QA
  P2-2, cso I3). The ask's own chat is named only when the platform stamped it
  in-turn; a background ask has no chat origin (its run still says where it
  came from).

Tests: test_ent610_chat_turn_asks.py (10, red first) + 2 in
test_ent610_ask_context.py (red first; the unverified-fallback case now carries
the in-turn stamp). Mutations, each red once: any trigger verifies; fallback
without the in-turn stamp; no top-level post-filter; no LIKE escape (total);
chat read drops cleared; chat read keeps the window (via include_ended); no SQL
prefilter.
…it by server time, stay in history past 7 days and arrive with the turn (ent#610 PR A2, round 2)

Round-2 review of the 09-30 ruling build (item 1, the tile in its chat):
- C1 (review P1): answering a tile within one poll of switching to its chat
  dropped its card and ent#468 confirmation — the reset watcher ran after the
  ids watcher. One watcher over (chat, waiting ids).
- C2 (review P2, codex C6/V1): a second live turn's ask sat above the first
  reply, and a browser clock ahead lifted an ask above the message that raised
  it. Only server times are compared now: a sent message is `local` and sits at
  the newest server time before it; a live reply carries its stored
  `created_at` (replyFromHistory → `at`).
- I3 / codex C3: the chat also reads its own chat-turn asks
  (`fetchChatTurnAsks`, GET /asks?chat_id=, no ended window); the store's
  fresher row wins for the same id; a read for a chat already left is dropped;
  a failed read keeps the store's tiles and warns.
- plan-design P1-B: the asks are read when a turn's reply lands, not left to
  the 20 s poll; a new waiting tile counts toward "jump to latest" (QA mobile F3).
- I5: while earlier messages are not shown, an ask older than the first row
  shown is left out instead of stacked at the top.
- design/QA P1: the history row was gray-400 on white (2.54:1) — META_INK_CLASS
  is gray-500 in light (4.83:1), which also fixes its two voice uses; the row's
  title wraps to two lines on a phone and carries its full text as a title;
  the tile is capped at a readable 40rem.

Tests: 13 new in portalChatAskTiles.spec.js (red first) + the reply's `at` in
portalReplyRateable.spec.js. Mutations, each red once (13): the reset watcher
back; local row as newest; no truncated drop; chat read ignored; no stale-chat
guard; chat read wins; gray-400 back; a reply without its time; reply drops
`at`; refresh does nothing; no arrival count; no hover title; send stamps the
browser clock.
…ad is waited for (Abilityai/trinity-enterprise#672)

Review finding on PR #3133: the docstring claimed the read is "done long
before any delivery outcome". That holds for the budget-bound in_progress
path; a fast outcome (pending_start, docker_unavailable) waits for it — a
cached listing read. Comment only.

Refs Abilityai/trinity-enterprise#672

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts it too; Inbox strip, dropped filter, focus, touch and keyboard (ent#610 PR A2, round 2)

Round-2 review of the 09-30 ruling build (items 2–4, and the pane):
- Client QA check 3 FAIL: Work is a platform-only tab, so a client never saw
  "N waiting on you · Open in Inbox" — the ruling gives platform users and
  clients the same answer. The line is one component (PortalAsksWaitingLine),
  mounted by Work and by Info for every principal (Info is on a client's rail).
- Wording names the noun: "1 ask / 5 asks waiting on you" (plan-design G).
- Work's empty state keeps the line in the same top row as the loaded state
  (it sat last under the empty copy and jumped ~181px — design F2, QA N2).
- Inbox: the agent strip draws from ONE agent, so a second agent's ask no
  longer pushes the list down (codex C5); a ?from= the page opens with is
  checked (the strip watcher was not immediate — QA mobile F5's silent
  everyone-list) and the head says "Nothing waiting from X · …"; arriving by an
  "Open in Inbox" link focuses the list, not the body (QA P2-3).
- Pane: a background ask's failed-read fallback no longer offers "Open the
  conversation" into Main (codex C4, frontend half).
- Sidebar: the ask and unread pills each say what they count to a screen
  reader (design F3 / plan-design F).
- Touch: the ask card's chips, note and Send are 44px on any coarse pointer,
  not only below sm (QA mobile F2: 28–34px at 768); the agent band steps aside
  on a short phone viewport so the keyboard leaves the tile visible (QA mobile F1).
- Dead code (review I2): PortalWork's `chatId` prop (no reader; the rail store
  scopes the children) and PortalAgentDetails' never-emitted `open-rail-tab`;
  stale "Waiting on you" comments. Review I1: the excludeAskIds case (a prop
  that no longer existed) and the comment-anchored guard are rewritten.

Tests: portalAsksWaitingLine.mount.spec.js + portalRound2Touch.spec.js (new),
cases added in portalWorkAsksLine / portalInbox.mount / portalInboxAgentFilter /
portalAskContext.mount / portalAgentNeedsYouMark.mount (red first); old pins
updated in portalWork / portalAskEndings / portalSignoffRound5. FE 4422/4422,
build, check:tokens, both ratchets. Mutations, each red once (12): strip only
at 2 agents; dropped filter not remembered; strip watcher not immediate; no
focus on arrival; fallback link for background asks; no sr text on the ask
pill; Send not 44 on coarse; band never steps aside; wording without a noun;
Info has no line; empty-state line gone from the top row; head never says the
filter was dropped.
…e line on Info, Inbox strip (ent#610 PR A2)

Review I4 drift fixed (core-agent Work description + AC-4, the dismantled page's asks row, workspace.md pending-only/7-day, workspace-agent-page, sidebar-ia 'sum' → same feed + sr labels). The round-2 behaviour documented where it is described: GET /asks?chat_id= (api-endpoints, security §26, workspace.md), the chat-turn-only origin (core-agent, security, workspace-inbox), tile placement by server time + the chat read (agents-at-the-centre), PortalAsksWaitingLine on Work and Info, the Inbox strip from one agent / dropped ?from= / focus. User docs that still said asks render above the composer or under Work's 'Waiting on you' now say where an ask lives per the 09-30 ruling.
…n line (ent#610 PR A2, round-2 re-check)

The live re-check measured the title at 122 of the row's 351px at 375 — the
kind, ending and time are shrink-0 — so even wrapped to two lines it clipped.
Below sm the row wraps: kind · ending · when on one line, the title full width
under it (the dot between kind and title is hidden there). Measured live at 375
in both themes: title 351px, not clipped.

Test: portalRound2Touch.spec.js (red first; declared source-text pin — a
responsive class jsdom does not evaluate). Mutation (drop max-sm:flex-wrap): red.
… row, announced, with focus on the row (ent#610 PR A2, team ruling 2026-10-01)

The team's ruling on #3101 (follow the 09-30 ruling): when the answer is
recorded the card collapses right away into the muted row — kind · title ·
ending · who · when. Round 2 had kept the answered card, with its "Sent.",
until you left the chat.
- askTileMode: pending → card, anything else → row (the seen-set exemption is
  gone). A failed answer leaves the ask pending in the store (answerAsk writes
  only on success), so its card stays open with PortalAsks' error.
- The row's confirmation ("Answered by you · just now") is announced through a
  persistent aria-live region; an ask already ended when the chat opened is not.
- Focus moves to the row (tabindex=-1, the design-system ring) when the card
  took it down with it; it never leaves the composer or anywhere else it sits.
- Live (1440, light, admin, a real answer through the tile): messages above
  0px, composer 0px, the next message up 154px = the card's own 170→16px
  change; focus on the row; announced; the row survives a chat switch; 0
  console errors.

Tests (red first): portalChatAskTiles.spec.js — collapse + announce + focus on
the row, no focus steal from a focused field, no announcement on open, the
switch-then-answer case (it must still be seen to be announced); the unit and
portalSignoffRound5 askTileMode cases reversed. Mutations, each red once: an
answered card stays a card; nothing announced; focus stolen from anywhere;
focus never moved; row not focusable. (Announce-on-first-paint is equivalent:
nothing leaves a chat's pending set on its first paint.)
…me (ent#610 PR A2 sign-off)

AskMarkdown's inline-code pill (prose-code:bg-gray-100) also landed on the
<code> inside typography's dark <pre> (light text), so a block read
light-on-light — Andrii's sign-off screenshot, the Inbox pane, light only
(dark's pill is gray-700). The pill is for inline code; a block's code now
sits on the block: [&_pre_code]:bg-transparent [&_pre_code]:p-0.
Live, light: code rgba(0,0,0,0) on the pre's rgb(31,41,55), text
rgb(229,231,235).

Test: askMarkdownCodeBlock.mount.spec.js (mounted, red first). Mutation (drop
the two classes): red. Pre-existing on dev (#3115); ChatBubble.vue carries the
same pill class but with indigo text, which stays readable — left alone.
…unced, focused row (ent#610 PR A2, team ruling 2026-10-01)
…hat agent has nothing waiting (ent#610 PR A2 sign-off)

Andrii's sign-off (2026-10-01): /workspace/inbox?tab=action&from=acme-sage
rewrote itself to ?tab=action and listed everyone's asks, saying so only in the
11px uppercase list head — it read as a silent redirect. His call: keep the
filter, show it empty.
- Arrival (the page opens on a ?from=, or the ?from= changes): kept even with
  nothing waiting — the URL keeps it, the facet stays chosen, the list says
  "Nothing is waiting on you from X." with "Show all agents" (→ ?tab=action).
- A filter answered down to nothing DURING the visit still clears (round 1),
  the head saying "Nothing waiting from X · …".
- listHeadLabel: an empty filtered Action says "Nothing waiting from X", not
  "All caught up".
Live (typed URL, light): URL kept, the empty copy and link, Show all → 31
rows, 0 page errors.

Tests (red first): portalInbox.mount — arrival keeps it (URL, no rows, copy,
link href, facet chosen); a mid-visit drain still clears with the head notice;
portalInboxAgentFilter — the head wording. Mutations, each red once: never
kept; a drained filter never clears; generic empty copy; generic head.
# Conflicts:
#	docs/memory/feature-flows/workspace-agents-at-the-centre.md
@vybe vybe added the ui PR touches the frontend UI — triggers Playwright e2e tests label Oct 1, 2026
vybe pushed a commit that referenced this pull request Oct 1, 2026
#3054 and #3137 landed as squashes, so dev re-carries content this branch
already contains. Each conflict resolved to this branch's side as integrated on
train #3143; the result tree is byte-identical to the CI-green train tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vybe vybe closed this Oct 1, 2026
@vybe
vybe deleted the train/20261001-0847 branch October 1, 2026 09:47
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.

4 participants