feat(members): chat-profile projection and escalation cards in the DM thread - #8614
Conversation
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of temp-screenshots/ is a pre-existing repo convention (many subdirectories at base), so that's not a finding. I have what I need for the design review. Design-Verdict: CONCERNS Sound projection-over-transcript design; the one real risk is the escalation answer rule now living twice, client and server, pinned in sync only by prose. Watch
Suggestions
[DESIGN-REVIEWED] fe58d7a |
UX Review (Fable 5) — 🟡 CONCERNSUX-level review of Reconciliation is complete. The decisive fact: the committed screenshots and gif were captured from an earlier commit (693f9d3, per the PR body's own URLs) — shot-03 shows the hint "Or type a reply below.", a string that no longer exists at HEAD, and option rows without the radio dots the shipped card draws. The blind reader, reading that stale artifact, confidently misread the options as send-on-click ("answer buttons; clicking one sends that choice as my reply") — exactly the misread the shipped design (radio dot, "Pick an option, then Send reply.", restated "Send '…'" button) claims to fix, but that fix appears in no committed pixel and got no blind read. Everything else reconciled well: countdown, default line, Needs you badge, Hide process, Working…, and the answered-option bubble were all correctly identified. UX-Verdict: CONCERNS The shipped pick-then-send card is unphotographed: every screenshot shows the superseded chip design the blind reader misread as send-on-click. Watch
Evidence gaps
Suggestions
[UX-REVIEWED] fe58d7a |
First Principles Review (Fable 5) — ✅ human override accepted@CrysisDeu overrode this lane for |
GPT 5.6 Review — ✅ human override acceptedHuman judgment by @CrysisDeu overrides the GPT 5.6 finding for This comment is updated in place on each push. The model was not re-run because an authorized human decision supersedes it. False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
916eda8 to
2e8d603
Compare
ad706f1 to
2fdd676
Compare
2e8d603 to
7e122ff
Compare
2fdd676 to
256ccd7
Compare
7e122ff to
c7e3957
Compare
c7e3957 to
de85a4a
Compare
256ccd7 to
9a55cdc
Compare
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
/ai-review override first-principles 52a2ade: Same finding as on 693f9d3, same ruling: the silent-rounds fold ships dark by design. Whether member slots may host auto-nudge loops is an owner-level product decision that lives in autonudge_authz.py, outside this PR; deleting a unit-tested, unreachable fold now only forces a delete-then-re-add across two reviews if the guard is lifted. The PR body says it ships dark. Tracked in #8908, which closes by either lifting the guard or removing the fold. |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
/ai-review override first-principles 36b1c16: Same finding as on 693f9d3, same ruling: the silent-rounds fold ships dark by design. Whether member slots may host auto-nudge loops is an owner-level product decision that lives in autonudge_authz.py, outside this PR; deleting a unit-tested, unreachable fold now only forces a delete-then-re-add across two reviews if the guard is lifted. The PR body says it ships dark. Tracked in #8908, which closes by either lifting the guard or removing the fold. |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
Two notes carried over from #8613's Design Review, for this PR's card (both are about copy/decisions, not #8613's backend):
(#8613's head this note refers to: |
|
/ai-review override first-principles e2d57a9: Same finding as on 693f9d3, same ruling: the silent-rounds fold ships dark by design. Whether member slots may host auto-nudge loops is an owner-level product decision that lives in autonudge_authz.py, outside this PR; deleting a unit-tested, unreachable fold now only forces a delete-then-re-add across two reviews if the guard is lifted. The PR body says it ships dark. Tracked in #8908, which closes by either lifting the guard or removing the fold. |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
/ai-review override first-principles 67d8765: Same finding as on 693f9d3, same ruling: the silent-rounds fold ships dark by design. Whether member slots may host auto-nudge loops is an owner-level product decision that lives in autonudge_authz.py, outside this PR; deleting a unit-tested, unreachable fold now only forces a delete-then-re-add across two reviews if the guard is lifted. The PR body says it ships dark. Tracked in #8908, which closes by either lifting the guard or removing the fold. |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
/ai-review override first-principles 436d6d4: Same finding as on 693f9d3, same ruling: the silent-rounds fold ships dark by design. Whether member slots may host auto-nudge loops is an owner-level product decision that lives in autonudge_authz.py, outside this PR; deleting a unit-tested, unreachable fold now only forces a delete-then-re-add across two reviews if the guard is lifted. The PR body says it ships dark. Tracked in #8908, which closes by either lifting the guard or removing the fold. |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
Follow-up from #8613 (head 516ed6a): the |
|
/ai-review override first-principles d068188: Same finding as on 693f9d3, same ruling: the silent-rounds fold ships dark by design. Whether member slots may host auto-nudge loops is an owner-level product decision that lives in autonudge_authz.py, outside this PR; deleting a unit-tested, unreachable fold now only forces a delete-then-re-add across two reviews if the guard is lifted. The PR body says it ships dark. Tracked in #8908, which closes by either lifting the guard or removing the fold. |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
/ai-review override first-principles cb05eab: Same finding as on 693f9d3, same ruling: the silent-rounds fold ships dark by design. Whether member slots may host auto-nudge loops is an owner-level product decision that lives in autonudge_authz.py, outside this PR; deleting a unit-tested, unreachable fold now only forces a delete-then-re-add across two reviews if the guard is lifted. The PR body says it ships dark. Tracked in #8908, which closes by either lifting the guard or removing the fold. |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
/ai-review override first-principles cd44572: Same finding as on 693f9d3, same ruling: the silent-rounds fold ships dark by design. Whether member slots may host auto-nudge loops is an owner-level product decision that lives in autonudge_authz.py, outside this PR; deleting a unit-tested, unreachable fold now only forces a delete-then-re-add across two reviews if the guard is lifted. The PR body says it ships dark. Tracked in #8908, which closes by either lifting the guard or removing the fold. |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
/ai-review override first-principles f838871: Same finding as on 693f9d3, same ruling: the silent-rounds fold ships dark by design. Whether member slots may host auto-nudge loops is an owner-level product decision that lives in autonudge_authz.py, outside this PR; deleting a unit-tested, unreachable fold now only forces a delete-then-re-add across two reviews if the guard is lifted. The PR body says it ships dark. Tracked in #8908, which closes by either lifting the guard or removing the fold. |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
… thread The Crew Members DM thread rendered the member's raw transcript: every tool row, notice and nudge between the human's message and the member's reply. That is the right view on the Sessions page and the wrong one for a conversation. This gives ChatPane a `displayProfile` — `transcript` (default, unchanged) or `chat` — and mounts the chat profile on the Members page. The chat profile is a projection over the same rows (pages/members/ chatProjection.ts), computed client-side so the view can never disagree with the transcript it is drawn from: * human messages, the FINAL assistant reply of each turn, escalation cards, approval and error rows stay; tool / thinking / notice / nudge / inject / subagent rows are hidden and attached to the reply as `meta.chat_process`, reachable through a "View process (N steps)" expander; * while the member runs a turn, a "Working: <tool purpose>" line stands in for the hidden rows (WS tool_call -> slotStatusDetail; falls back to the tool name, then a generic line); * consecutive auto-nudge rounds whose reply said nothing new fold into one "N check-ins with nothing new" row with its own expander. (Implemented and unit-tested; a member thread cannot host an auto-nudge loop today because autonudge_authz refuses member-mode slots, so this ships dark until that guard is revisited.) Escalation rows (role `escalation`, from session_send target=user) render as a card: title, sender, goal chip, markdown body, a live countdown to the deadline, the default action if unanswered, and option chips that send the chosen text as the human's reply. Card state is derived on the client — answered when a later user row exists, expired when the deadline passed — so it needs no extra fetch. The roster row shows a "Needs you" badge from the slot frame's `needs_you`. A read-only card variant is registered in the SDK defaults and on the Sessions page so the row is never drawn raw. MembersPage.tsx changes are three small hunks (isNeedsYou, the badge, the displayProfile prop) to stay clear of the in-flight remember-last-member and driving-sessions work in the same file. i18n: 21 keys under pages.members.chat in all 12 catalogs + pseudolocale, with per-language plural forms (CJK _other only; es/fr/pt/it _many; ru _few/_many); bases registered in pluralKeys.json. Evidence: temp-screenshots/crew-chat/ — GIF recorded on an isolated pod against a real member session and real model turns, no fixtures.
|
/ai-review override first-principles fe58d7a: Same finding as on 693f9d3, same ruling: the silent-rounds fold ships dark by design. Whether member slots may host auto-nudge loops is an owner-level product decision that lives in autonudge_authz.py, outside this PR; deleting a unit-tested, unreachable fold now only forces a delete-then-re-add across two reviews if the guard is lifted. The PR body says it ships dark. Tracked in #8908, which closes by either lifting the guard or removing the fold. |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
/ai-review override gpt fe58d7a: Cross-pane collision only appends a visible, deletable option label onto a preserved draft (not silent/irreversible loss), and needs a compound race of two panes sending the identical option with interleaved pre-receipt cancels. |
Human judgment recorded@CrysisDeu marked the gpt AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
Summary
The Crew Members DM thread rendered the member's raw transcript — every tool row, notice and nudge between the human's message and the member's reply. That is the right view on the Sessions page and the wrong one for a conversation. This PR gives
ChatPaneadisplayProfile(transcript, the unchanged default, orchat) and mounts the chat profile on the Members page.recorded from
693f9d3b5a327c73d4d9b698369cf4735f54166b· isolated podwt-escalation, seeded config, crew "Radar" on the KAS backend · mode:--approval yolo· real server + real model turns, no fixturesWhat the chat profile does
pages/members/chatProjection.tsis a pure projection over the same rows, computed client-side so the view can never disagree with the transcript it is drawn from:meta.chat_process— one click on View process (N steps) shows them.WS tool_call → slotStatusDetail; falls back to the tool name, then a generic line). It is the only busy indicator in the chat profile — the footer's ghost carousel is suppressed there (stopping/compacting states still show).autonudge_authzrefusesmember-mode slots, and the member'skirocrew-corehas no identity channel on KAS). Whether to lift that guard for member-armed loops is a separate decision.Escalation cards
role: escalationrows (from #8613'ssession_escalate/POST /api/session-control/escalate) render as a card: title ("Radar needs you"), sender, goal chip, markdown body, a live countdown to the deadline, the default action if unanswered, and the options as a vertical single-choice list with one Send reply button (the same shape asQuestionCard; double-click an option to select and send in one gesture). The pick-then-send step is drawn, not implied: each option carries a radio dot, the button restates the pick once one is made (“Send ‘Hold until Monday’”, clipped to a phrase with the full text as its title), and until something is picked the hint beside it reads “Pick an option, then Send reply.” — so a click on an option is never mistaken for the reply. The reply carriesmeta.escalation_id, so the backend answers exactly that record — and because it is addressed by id,ChatPane.doSenddoes not treat it as the answer to whatever stateless question card or blocking ask happens to be pending on the slot (no capture, no retire): a reply to the member's escalation cannot silently dismiss an unrelated pending question. Card state is derived on the client by the same rule the index applies (#8613): a reply carrying the id answers that card; a free-text reply answers only when exactly one escalation is pending; a reply after the deadline never answers; the 1-second tick flips a live card to expired / defaulted ("Default applied: …") the moment the clock passes the deadline. The roster row shows a Needs you badge from the slot frame'sneeds_you. Every other surface keeps drawing #8613's read-onlyEscalationNotice(the SDK default), which points at the Members thread; this card is the ONE place an escalation is answered. The conversation index (GET /api/members/{slug}/conversation) is the authority on which escalation is still open; when that request fails before any record has loaded the cards fall back to the client-side rule, and when a REFETCH fails the last good record stays authoritative (a stalependingbeats a window simulation that could read a truncated transcript as answered); either way an inlineErrorNoticeunder the thread says so (title + the raw error, noaskAgent: the hand-off would unmount the pane and the composer draft in it), and the 20 s safety-net poll keeps retrying while a card is pending. An optimistic (not yet acknowledged) reply never counts as an answer, so a refused send leaves the card answerable. A reply the pane had to queue behind a running member turn keeps the card latched for as long as its queue entry exists; when the entry leaves the stack the latch lets go only if the server confirmed the cancel (useQueuedMessageActionsnow also exposescancelledIds, the ids whose DELETE resolved) — the stack removes a cancelled entry optimistically, so an entry that is merely absent may still be queued behind a failed cancel and unlocking on absence alone would let a second reply run on top of the first. Unconfirmed, the authority (the index or the confirming row) is the only release. The same rule covers an uncertain acceptance — a 2xx the pane could not read, or one that arrived after the abort deadline (SendOutcome.uncertain): the reply may be queued under an id the host never learned, so the card drops its 45 s valve and waits for the authority instead of unlocking on a clock. One invariant, stated once: a reply the server may hold is never released by a timer or by an entry's absence — only by the index, the confirming row, or a server-confirmed cancel. Cancelling such a queued reply from the stack restores the pick on the card and nothing to the composer: the option send binds an empty pre-send record (queuedSendStash), so the cancel path takes the stash hit instead of parsing the row and appending the option label to the draft. For an option send only, that empty record is also registered under the send's ownsendId(inFlightSendStash) BEFORE the request goes out and moved under the queue id when the receipt names one, so a cancel that lands in thequeue_push-before-receipt gap still finds it by wire text — safe precisely because the record is empty by construction (typed sends, whose text and files could be mis-restored by a wire-text collision, stay bound by queue id only); and a server-confirmed cancel releases the card's latch even for an entry the stack never showed it, so that same gap cannot leave the card locked with no way out.Coordination
MembersPage.tsxis touched in three small hunks only (isNeedsYou, the badge, thedisplayProfileprop) to stay clear of the in-flight remember-last-member and driving-sessions work in the same file. Everything else is new files underpages/members/plus adisplayProfileseam inChatPane;messageRenderers.tsxis untouched (itsescalationdefault stays #8613's notice).i18n
26 keys under
pages.members.chatin all 12 catalogs + regenerated pseudolocale, with per-language plural forms (CJK_otheronly; es/fr/pt/it_many; ru_few/_many); plural bases registered inpluralKeys.json; translator context inen.context.json. All 19i18n:checkgates pass againstorigin/main.Tests
chatProjection.test.ts(11) — hiding, final-reply selection, process attachment, streaming kept while running, fold of 3 silent rounds, non-quiet round not folded, escalation/permission keptEscalationCard.test.tsx(18) — body/options, select + Send →onSend(text, {escalation_id}), double-click quick send, radio dot + restated Send label + pick hint, long-option clipping, expired/defaulted (fake timers) closes the card and swaps the default line to "Default applied", live countdown and clock-crossing flip, answered badge,formatRemainingchatProfileRenderers.test.tsx(6) — theassistantoverride wins aftermergeRenderers, process disclosure, fold row, chips send via host handler, the SDK default alone draws feat(session-control): escalate to the human as a peer (session_escalate) #8613'sEscalationNotice(not this card)useEscalationIndex.test.tsx— fetch/poll/abort/dedupe, anderroris set only by a FAILED request (null while loading, on success, and on a non-member slot)MembersPage.test.tsx— badge from live slot and from roster fallback; ChatPane receivesdisplayProfile="chat"tsc -b, eslint, and the full i18n gate set are clean locally; the vitest suite forsrc/pages/members+src/i18n+ the ChatPane working-loader harness passes (732 tests).Stack
fix/member-dispatch-homefeat/crew-conversation-indexfeat/escalation-user-peer(base of this branch)feat/crew-chat-profile