A crowd you can find, and a phone 65px too narrow

Two reports against 1.6.0 and 1.7.0, both correct.

The crowd worked end to end and was, in practice, not there: the picker was
behind the ⋯ menu of a chat that already existed, and the switch was a card on
the Agents page, which made it read as an agent-chat feature. The picker is now
a button in the composer toolbar on both screens that include it, and on the
new-chat screen the choice rides along with the first message, so a chat can
start as a crowd instead of having to be converted into one. The instance
switch has its own page.

The width bug was the suggestion cards, exactly as reported. `.suggestions`
rendered 455px inside a 366px column, and the tree's standing rule applied on
its own made it worse -- 428px to 455px. A grid item carries `min-width: auto`,
which is a min-content floor, and a floor beats `width: 100%`; the floor is
measured while the percentage is indefinite, so `min(100%, …)` alone sends the
track to a card's max-content. Both halves now go on all four auto-fit grids,
and a test refuses either alone.

It survived four releases of narrow-width checking because the harness never
rendered that screen: `TestClient(app)` runs no lifespan outside a `with` block,
so the startup-seeded cards were missing from every shot ever taken of it. And
its overflow check skipped anything inside a scroller -- right for a table in
its own scroller, blind to the scroller itself, which `overflow-y: auto` makes
scroll sideways too. Both fixed; it now names the box and the child to blame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 20:08:10 +00:00
co-authored by Claude Opus 5
parent a16510aba8
commit ab32c68a8f
18 changed files with 714 additions and 160 deletions
+87
View File
@@ -0,0 +1,87 @@
{% extends "admin/_layout.html" %}
{% from "_macros.html" import icon %}
{% set section = "crowd" %}
{% block title %}A crowd - {{ brand.name }}{% endblock %}
{% block heading %}A crowd{% endblock %}
{% block admin_content %}
{#
Its own page rather than a card on Agents, which is where it shipped in 1.6.0.
Sitting there made it read as an agent-chat feature -- the owner took it for one,
reasonably, because that is what the page is called -- and a crowd has nothing to
do with agent chats: it works in any conversation.
#}
<p class="admin-lede">{{ t("Several models answering one turn, in any chat. Not an agent-chat feature: it works in an ordinary conversation, and the control is in the composer beside the tool switches.") }}</p>
{% if saved %}
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ t("Settings saved.") }}</span></div>
{% endif %}
<form method="post" action="/admin/crowd" class="form-grid">
<section class="card">
<p class="field__hint">
A chat can have more than one model in it. The chat's own model answers, then
each of the others in turn; then the order runs <strong>{{ t("backwards") }}</strong>,
each one asked whether it disagrees with anything; and it ends back at the
first, which either closes or sends them round again.
</p>
<div class="alert">
{{ icon("warning", "icon--sm") }}
<span>
One turn costs <strong>{{ t("models × rounds × 2 − 1") }}</strong> replies — four
models over two rounds is fifteen — and on a single local endpoint every
change of speaker also loads a different model. Larger crowds of smaller
models, and sometimes of bigger ones, start going round in circles: that is
what the round limit is for, and it is a limit ordinary work will reach
rather than a runaway backstop.
</span>
</div>
<div class="field">
<label class="checkbox">
<input type="checkbox" name="enabled" value="true"
{{ 'checked' if crowd.enabled }}>
<span>{{ t("Let a chat have a crowd") }}</span>
</label>
<p class="field__hint">{{ t("Off by default. With it on, every chat's composer offers the other models; a chat with none ticked behaves exactly as it always has.") }}</p>
</div>
<div class="field">
<label class="field__label" for="crowd_max_models">{{ t("Most models besides the chat's own") }}</label>
<input class="input" id="crowd_max_models" name="max_models"
type="number" min="1" max="8" step="1" value="{{ crowd.max_models }}">
<p class="field__hint">{{ t("Four is already eight replies a turn at one round each. More voices past that tend to repeat each other rather than add anything.") }}</p>
</div>
<div class="field">
<label class="field__label" for="crowd_max_rounds">{{ t("Most rounds") }}</label>
<input class="input" id="crowd_max_rounds" name="max_rounds"
type="number" min="1" max="5" step="1" value="{{ crowd.max_rounds }}">
<p class="field__hint">{{ t("A round is out and back. Two gives the first model one chance to change its mind after hearing the objections, which is the point of the whole thing; three is where going in circles starts.") }}</p>
</div>
<div class="field">
<label class="field__label" for="crowd_wall_seconds">{{ t("Longest a turn may take") }}</label>
<input class="input" id="crowd_wall_seconds" name="wall_seconds"
type="number" min="60" max="7200" step="30" value="{{ crowd.wall_seconds }}">
<p class="field__hint">{{ t("Across every speaker, not each. A member whose endpoint has stalled cannot then hold the round open all afternoon.") }}</p>
</div>
<div class="field">
<label class="checkbox">
<input type="checkbox" name="collapse_agreement" value="true"
{{ 'checked' if crowd.collapse_agreement }}>
<span>{{ t('Fold away a short "I agree" on the way back') }}</span>
</label>
<p class="field__hint">{{ t("The disagreements are what a crowd is for; a column of bubbles saying nothing is what makes somebody switch it off. The text is still there behind a disclosure.") }}</p>
</div>
<div class="btn-row">
<button class="btn btn--primary" type="submit">{{ t("Save") }}</button>
</div>
</section>
</form>
{% endblock %}