Skip to content

feat(v2): a pod with no agent says so, and offers the one step - #1846

Merged
lilyshen0722 merged 3 commits into
mainfrom
fix/task-146-empty-pod-add-agent
Sep 24, 2026
Merged

lilyshen0722 merged 3 commits into
mainfrom
fix/task-146-empty-pod-add-agent

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Closes TASK-146, built to ux-lead's draw on the row.

The gap

podChat.deliveryHint — "No agent was notified — @mention one to get a reply" — is gated on delivery.agentsInPod > 0 (V2Thread.tsx:1317), because it needs a handle to suggest. So the pod with zero agents is the one case it skips, and it is the case where the user most needs the sentence: they posted, nothing answered, and the starter panel's "Add an agent" act is already gone (starterPanelVisible requires messages.length === 0). Lily saw exactly this in a real signup — pod created, one post, silence, then a second pod.

What it does

When a send returns agentDelivery.agentsInPod === 0 in a non-DM pod, a system row renders in the same slot, directly under the sent message:

  • mono-11 kicker no agent in this pod
  • "Nobody here answers yet — people will see this when they open the pod."
  • one bordered act, Add an agent — min-height: 32px on desktop, full width and min-height: 44px at ≤760 — opening the same sheet the starter panel opens, on its agent tab (onOpenInvite('agent')), where Scout is first and connect-your-own second.

Ink and tint only: no accent, no toast, and it is not a role="alert".

Once per pod per browser session, like the delivery hint — but on its own key (v2.noAgentsHint.<podId>, distinct from v2.agentDeliveryHint.<podId>), because both rows can legitimately occur in the same pod in one session: the no-agent row before an agent is added, the mention hint after. Sharing a key would let either cancel the other.

It goes when an agent joins, not on the next send: the row is gated on the live pod detail (agents.some(status === 'active'), the client's counterpart of the server's AgentInstallation.countDocuments({ podId, status: 'active' })), so the pod-detail re-read removes it.

Deliberate exclusions (stated, not incidental)

  • DM pods (agent-room, agent-dm): both have an agent by construction, and their empty states already name who is missing.
  • No invite surface available (onOpenInvite undefined, i.e. a pod type the shell cannot invite into): the row is explanation plus the one step. Without the step it would be a sentence pointing at nothing the user can do, so the row stays hidden rather than half-rendering.

Proof

V2ThreadDeliveryHint 15/15 (7 new), and 169/169 across 17 suites that mount the thread or the shell, v2-layout-invariants 109/109 (1 new).

mutation result
remove the new branch 4 new tests red
drop the live-guard on render "goes as soon as an agent joins" red
stop excluding DM pods both DM cases red
share one session key with the delivery hint the silent-send test and the same-session hint test red
render without a way to add an agent "stays hidden when the shell has no way" red
act opens the sheet on the default tab the click assertion red
phone override pins height: 44px instead of min-height the layout guard red

The last one is the TASK-140 lesson: a height override loses to the base rule's own min-height: 32px and renders 32 on a phone. The guard asserts min-height: 44px and not /[^-]height: 44px/, and it reads the ≤760 block through a new mediaBlockContaining helper — ruleBody prefers a line-start selector, so it would have pinned the desktop rule and gone green on a broken phone layout.

Limit: the jsdom tests cannot see the rendered 390 layout — the mono kicker, the row's alignment under the message, and the 44px act are ux-lead's visual gate at 1440 + 390. The row also persists only until reload/session end (it is a once-per-session nudge like the hint beside it), and a pod whose detail has not yet loaded reads as empty, so the guard uses the loaded list rather than a server count.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: FAIL @ 6ae3bb0 — TASK-146, gated at 1440 + 390 on a production build of this head with the 1844 fixture plus an open team pod with zero agents (POST returns agentDelivery.agentsInPod 0). One blocker, in the CSS: the row inherits width: 100% from .v2-chat__messages > * (v2.css:1810) and keeps its own margin-left: 50px, so it is 100% + 50 wide and the transcript's overflow-x: hidden clips it. At 390 the full-width act renders 362 wide from x=64 to 426 on a 390 viewport (36 px of the button off-screen) and the body's line end is cut (…when they op); at 1440 the act is 99 px so nothing visible breaks, but scrollWidth is 830 in a 780 pane. Fix verified by style injection: .v2-chat__messages > .v2-chat__no-agents { width: calc(100% - 50px); } → 312 wide at 390 ending at the message text's right edge (376), 682 at 1440 ending at 1115, scrollWidth = clientWidth at both. Pin it with a width assertion in the invariants test (jsdom cannot see the overflow; the existing .v2-chat__delivery-hint has the same 100% + 50 clip — pre-existing, out of this diff, same one-line fix if you want to fold it in). Everything else passes: kicker Plex Mono 11 lowercase tertiary; body 12 secondary; act bordered white, ink, 12/600, radius 4, no shadow, min-height 32 at 1440 and 44 full-row at 390 (the min-height override holds); role=status; one row per pod per session (second send adds none; v2.noAgentsHint.pod-quiet=1 survives reload and suppresses); the act opens the invite modal with the Add agent tab selected; a pod with an active agent gets the delivery hint, not this row; no green anywhere. Taste, non-blocking: the slot's 50 px margin lands 14 px right of the message text column at both widths (row x 433 vs text 419 at 1440; 64 vs 50 at 390) — same as the delivery hint today; a slot-level fix later, not this PR.

@samxu01
samxu01 force-pushed the fix/task-146-empty-pod-add-agent branch 2 times, most recently from 37f374c to 09358ff Compare September 23, 2026 10:04
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Fix for the UX-GATE FAIL at 6ae3bb01, applied at 09358ff0 (rebased onto main 0c4f9507; main's move was backend/__tests__/unit/scripts/npmPublishReadback.test.js only — zero overlap, so no suite re-run was warranted).

Exactly as ruled: width: calc(100% - 50px) on .v2-chat__no-agents, because .v2-chat__messages > * sets width: 100% and the row's 50px indent otherwise puts it 50px past the pane — at 390 that is the act running 64→426 with the body line cut.

The precedent is in this same file: .v2-thread-block carries the identical pair with Sam's 2026-08-24 note that indent + width must sum to 100%, from when threads became horizontally swipeable at 390 (scrollWidth 350 vs clientWidth 326). So this is that rule one indent over, not a patch for one screenshot.

Pinned in v2-layout-invariants: the guard asserts the column rule still sets width: 100% and the row subtracts the indent. Two mutations, both red on the guard alone — deleting the width line, and setting width: 100% back (which reproduces your 390 overflow as a failure). Restored → 109/109.

Flagged, not fixed: the pre-existing .v2-chat__delivery-hint beside it has the same 50px indent and no width, so it carries the same geometry. It survives the shot only because its sentence breaks before the overhang; a longer string at 390 would clip. Outside this diff — your call whether it is a follow-up.

Re-gate requested at 09358ff0.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ 09358ff — re-gate on a fresh production build of this head (index-DgYSS794) with the same zero-agent fixture pod, 1440 + 390. Delta over my scope since 6ae3bb0 is exactly the ruled fix: width: calc(100% - 50px) on .v2-chat__no-agents (patch-id over components + v2.css + locales b4d963d1 at merge-base 0c4f950; the other two files are tests, one of them backend from the rebase). Measured: 390 row 64→376 = the message text's right edge, act 312×44 fully on-screen, body line whole, transcript scrollWidth 390 = clientWidth; 1440 row 433→1115 = text right, act 99×32, scrollWidth 780 = clientWidth. Kicker 11 mono, act white/ink/radius 4/no shadow, one row per session, all unchanged from the first pass. The .v2-thread-block precedent cited in the comment is the right one. Delivery hint's identical 50 px indent without a width stays a follow-up, not this PR.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GATE: PASS at 09358ff0 — measured. Two gaps, both non-blocking: the code is right, the safety clears are unpinned.

BASE 124/124 (V2ThreadDeliveryHint + v2-layout-invariants), RESTORED 124/124, tree clean after every mutation. Anchor count 1 on each; the two ambiguous anchors were line-scoped.

What is pinned:

mutation on V2Thread.tsx result
M1 delivery.agentsInPod === 0 → >= 0 1 red
M2 drop the !isAgentDm exclusion 1 red
M3 remove the once-per-pod guard (line 1369) 1 red
M5 noAgentsHint={noAgentsInPodLive ? … : null} → unconditional 2 red

Separating the two sessionStorage keys is the right call, and an agent joining does not suppress the mention hint in the same session is the test that makes it more than a comment.


Gap 1 — the typing clear (V2Thread.tsx:1149) is unpinned, and it is the #914 case

Removing setNoAgentsHint(null) from the agent_typing_start handler leaves 124/124 green. The existing #914 test covers only agentDeliveryHint; nothing covers the new row.

This is not an equivalent mutant — noAgentsInPodLive does not mask it, because a typing agent need not yet be in the agents prop. Measured with a probe (passes at head, fails under the mutation): render with agents: [] and agentsInPod: 0, send, then fire agent_typing_start. At head the row goes; with the line removed, no agent in this pod sits above the landing reply — exactly the defect your own comment on line 1147 says must not happen.

act(() => { handlers.agent_typing_start({ podId: 'pod-1', agentName: 'guide', displayName: 'Guide' }); });
await waitFor(() => expect(screen.queryByText('no agent in this pod')).not.toBeInTheDocument());

Gap 2 — the pod-switch clear (V2Thread.tsx:548) is unpinned

Same result: removing it leaves 124/124 green. Also reachable, and the effect is keyed on [pod?._id], so the component survives a pod switch and the state really does persist.

Probe (passes at head, fails under the mutation): send in pod-1 and earn the row, rerender into pod-2 with its own transcript, then return to pod-1. With the line removed the stale row reappears on return, which the once-per-pod intent forbids. The harness in the existing suite hardcodes pod-1, so this one needs a podId-parameterised harness.

Neither blocks the merge. Both are one test each, and both defend a sentence you have already written as a comment.

@samxu01
samxu01 force-pushed the fix/task-146-empty-pod-add-agent branch from 9c58e74 to 1ad20eb Compare September 23, 2026 10:16

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both gaps closed. PASS stands at 1ad20eb9.

  • Production code is untouched since the head I gated. V2Thread.tsx, V2ThreadMessages.tsx, v2.css and en.json are byte-identical to 09358ff0. The cli/ files in a raw two-head diff are main's #1840 arriving, not this PR — the PR-only delta against the new merge-base is the one test file, as you said. So the earlier mutation results carry, and ux-lead's stamp has nothing to re-measure.
  • BASE 126/126, RESTORED 126/126, tree clean.
  • Removing the pod-switch clear (:548): 1 red. Removing the typing clear (:1149): 1 red. Each reds exactly its own new test and nothing else, so the two are independent rather than one test covering both paths.

Keeping agents: [] in the typing test is the detail that matters — it is what stops noAgentsInPodLive masking the assertion, which is the whole reason the gap survived the first suite. Nothing further from me.

Lily's smoke: created a team pod with no agent, posted once, got nothing,
created a second pod with an agent. The existing delivery hint ("No agent was
notified — @mention one") is gated on agentsInPod > 0 because it needs a
handle to suggest, so this exact pod is the one it skips — and the starter
panel's "Add an agent" act is already gone once a message exists.

When a send returns agentsInPod === 0 in a non-DM pod, a system row renders in
the same slot under the sent message: mono-11 kicker "no agent in this pod",
"Nobody here answers yet — people will see this when they open the pod.", and
one bordered act, Add an agent (32 desktop / full-width 44 at <=760) opening
the starter panel's own sheet on its agent tab.

Once per pod per browser session on its own key, so it and the mention hint
cannot cancel each other — both can happen in one pod in one session. It goes
when an agent joins, not on the next send: gated on the live pod detail.

Excluded deliberately: DM pods (an agent by construction; their empty states
name who is missing) and pods with no invite surface (the row is explanation
plus the one step, so half of it is worse than none).

The row's 50px indent has to come out of its width: the column's `> *` rule
sets width: 100%, so without `width: calc(100% - 50px)` it overruns the pane by
exactly 50px (ux-lead's 390 gate: the act ran 64->426 and the body line was
cut). Precedent in the same file — `.v2-thread-block` carries the identical
pair with Sam's 2026-08-24 note that indent + width must sum to 100%.

Tests: V2ThreadDeliveryHint 15/15 (7 new), 169/169 across 17 thread/shell
suites, layout invariants 109/109 (1 new). Nine mutations, each red on the
expected test — including the phone override as `height` instead of
`min-height` (would lose to the base 32 minimum, TASK-140) and removing the
width, which reproduces ux-lead's 390 overflow as a guard failure.
sprint-review's gate on 09358ff passed but measured two silent-deletion paths,
one test each:

- removing `setNoAgentsHint(null)` from the typing handler left all 15 green.
  This is the #914 shape: the row must not sit above a reply that is about to
  land, and a typing agent need not be in the pod detail's `agents` prop yet, so
  the live guard cannot mask it — the probe shows the row staying put. The new
  test keeps `agents: []` for exactly that reason, so only the clear can pass it.
- removing the clear from the pod-change effect left all green too: leave the
  pod and come back, and the row is resurrected under a message the user did not
  just send, with pod-1's session key already spent so no send could re-earn it.

Both are behaviour already implemented and documented in comments; neither was
defended. Each mutation reds exactly one test — the new one — so neither is an
equivalent mutant. V2ThreadDeliveryHint 17/17.
@samxu01
samxu01 force-pushed the fix/task-146-empty-pod-add-agent branch from 1ad20eb to 2278186 Compare September 23, 2026 23:17

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ 2278186 — re-pin after rebase onto 38b6ea0: patch-id b4d963d1 over components + v2.css + locales unchanged.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ 2747d12 — re-pin only. Head moved by a merge of main (4a0661d, #1844) through no gated file; scoped patch-id over frontend/src/v2/components + v2.css + i18n/locales is b4d963d1, identical to the PASS at 2278186. No re-measure needed.

@lilyshen0722
lilyshen0722 merged commit 116ac7c into main Sep 24, 2026
15 checks passed
samxu01 pushed a commit that referenced this pull request Sep 24, 2026
…ASK-150)

The delivery cue and the no-agent row both hang under the sent message on
the message text column. Their box was declared twice: the cue had the
50px indent without the width complement, so under
`.v2-chat__messages > * { width: 100% }` it overran the pane by exactly
50px (#1846 fixed the same defect on the no-agent row).

Both rows are siblings of `.v2-msg` inside `.v2-chat__messages`, so the
indent is that grid's avatar column plus its gap. One rule now owns both
boxes, and v2-layout-invariants derives the expected indent from
`.v2-msg`'s own grid rather than restating 50 — so moving that column
fails the suite instead of parting the two edges silently.
samxu01 pushed a commit that referenced this pull request Sep 24, 2026
…ASK-150)

The delivery cue and the no-agent row both hang under the sent message on
the message text column. Their box was declared twice: the cue had the
50px indent without the width complement, so under
`.v2-chat__messages > * { width: 100% }` it overran the pane by exactly
50px (#1846 fixed the same defect on the no-agent row).

Both rows are siblings of `.v2-msg` inside `.v2-chat__messages`, so the
indent is that grid's avatar column plus its gap. One rule now owns both
boxes, and v2-layout-invariants derives the expected indent from
`.v2-msg`'s own grid rather than restating 50 — so moving that column
fails the suite instead of parting the two edges silently.
samxu01 pushed a commit that referenced this pull request Sep 24, 2026
…ASK-150)

The delivery cue and the no-agent row both hang under the sent message on
the message text column. Their box was declared twice: the cue had the
50px indent without the width complement, so under
`.v2-chat__messages > * { width: 100% }` it overran the pane by exactly
50px (#1846 fixed the same defect on the no-agent row).

Both rows are siblings of `.v2-msg` inside `.v2-chat__messages`, so the
indent is that grid's avatar column plus its gap. One rule now owns both
boxes, and v2-layout-invariants derives the expected indent from
`.v2-msg`'s own grid rather than restating 50 — so moving that column
fails the suite instead of parting the two edges silently.
lilyshen0722 added a commit to thantiklermcirony/commonly that referenced this pull request Oct 9, 2026
…ASK-150) (Team-Commonly#1860)

* fix(chat): one shared slot rule for the transcript's two side-rows (TASK-150)

The delivery cue and the no-agent row both hang under the sent message on
the message text column. Their box was declared twice: the cue had the
50px indent without the width complement, so under
`.v2-chat__messages > * { width: 100% }` it overran the pane by exactly
50px (Team-Commonly#1846 fixed the same defect on the no-agent row).

Both rows are siblings of `.v2-msg` inside `.v2-chat__messages`, so the
indent is that grid's avatar column plus its gap. One rule now owns both
boxes, and v2-layout-invariants derives the expected indent from
`.v2-msg`'s own grid rather than restating 50 — so moving that column
fails the suite instead of parting the two edges silently.

* fix(chat): the side-row slot is 36px — the thread's grid, not the base message grid (TASK-150)

ux-lead's Team-Commonly#1860 gate failed the indent: 50px put both side-rows 14px right
of .v2-msg__content at 390, 720 and 1440.

They are right, and the mistake was mine in a way the guard could not see.
The first cut derived the indent from the base .v2-msg rule (38 + 12 = 50)
and read the number off the rule that governs the message grid — but these
rows only ever render as siblings of a message inside .v2-thread, where
'.v2-thread .v2-msg { grid-template-columns: 28px minmax(0, 1fr); gap: 8px }'
(0,3,0) replaces the base grid (0,1,0). 28 + 8 = 36, and 50 - 36 is exactly
the 14px the fixture measured. Same class of trap as ruleBody preferring a
line-start selector: the helper read a real rule, just not the one in play.

Both placements of these rows share their message's container — transcript
level, or inside a .v2-thread-block for a thread root — so the relative
indent holds in both.

The guard now derives from .v2-thread .v2-msg instead, and asserts the base
grid is 38px specifically so a future reader cannot 'correct' 36 back to 50.
Five mutations, each applied in isolation with the restore verified against
the pre-mutation content (36px present, no diff): thread lead 28->32 (reds
the derivation and the existing avatar-column test), thread gap 8->10 (reds
the two slot tests), slot 36->50 (reds the two slot tests), slot 36->32
(the replies-tier mistake, reds the two slot tests), base grid 38->42 (reds
only the new base-is-not-the-target assertion).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant