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:
@@ -50,6 +50,13 @@
|
||||
{{ icon("sliders", "icon--sm") }}
|
||||
<span class="nav-item__label">{{ t("Models") }}</span>
|
||||
</a>
|
||||
{# Its own entry rather than a card on Agents, where it started. Sitting
|
||||
there made it read as an agent-chat feature -- which is what the owner
|
||||
took it for, reasonably, since that is what the page is called. #}
|
||||
<a class="nav-item {{ 'is-active' if section == 'crowd' }}" href="/admin/crowd">
|
||||
{{ icon("users", "icon--sm") }}
|
||||
<span class="nav-item__label">{{ t("A crowd") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'audio' }}" href="/admin/audio">
|
||||
{{ icon("speaker", "icon--sm") }}
|
||||
<span class="nav-item__label">{{ t("Audio") }}</span>
|
||||
|
||||
@@ -395,76 +395,6 @@
|
||||
button was pressed, which is what keeps each group's save handler writing one
|
||||
key.
|
||||
#}
|
||||
{# A third settings group on this page, saved by its own form -- the reason the
|
||||
Helpers card gives. A crowd is not an agent-chat feature either, but this is the
|
||||
page somebody opens to find out what one turn may set going. #}
|
||||
<form method="post" action="/admin/agents/crowd" class="form-grid">
|
||||
<section class="card">
|
||||
<h2 class="card__title">{{ t("A crowd") }}</h2>
|
||||
<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, each chat's settings panel 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>
|
||||
|
||||
<form method="post" action="/admin/agents/subagents" class="form-grid">
|
||||
<section class="card">
|
||||
<h2 class="card__title">{{ t("Helpers") }}</h2>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -293,6 +293,97 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{#
|
||||
Who else answers.
|
||||
|
||||
Beside the tool switches rather than buried in Chat settings, and
|
||||
*inside this form* rather than in the topbar, for one reason each.
|
||||
|
||||
The first: somebody deciding who answers is making the same kind of
|
||||
choice as somebody picking the model, and the first version of this
|
||||
put it only in the Chat settings panel — behind the ⋯ menu, inside a
|
||||
chat that already existed. The owner enabled the feature, went
|
||||
looking, and could not find it. A control nobody can find is a
|
||||
feature nobody has.
|
||||
|
||||
The second: on the new-chat screen there is no chat row to attach
|
||||
anybody to, so the choice has to *ride along with the first message*
|
||||
— which means being a field of this form. That is the same mechanism
|
||||
the scope switches above use, with the same hidden-input trick,
|
||||
because a browser submits only the ticked boxes and `start_chat`
|
||||
needs to know which ones were not.
|
||||
#}
|
||||
{% if crowd_available %}
|
||||
<div class="picker picker--up" data-picker>
|
||||
<button class="btn btn--icon composer__btn" type="button" data-picker-toggle
|
||||
aria-haspopup="menu" aria-expanded="false"
|
||||
aria-label="{{ t('Crowd') }}" title="{{ t('Crowd') }}">
|
||||
{{ icon("users") }}
|
||||
{% if crowd_member_ids %}
|
||||
<span class="composer__count">{{ crowd_member_ids|length + 1 }}</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
|
||||
<div class="picker__menu picker__menu--scope" data-picker-menu role="menu"
|
||||
hidden aria-label="{{ t('Crowd') }}">
|
||||
<p class="picker__lede">
|
||||
{{ t("Tick a model to have it answer after this one, then be asked whether it disagrees.") }}
|
||||
</p>
|
||||
<p class="picker__group">{{ t("Also answering") }}</p>
|
||||
{% if chat %}
|
||||
{# One hidden field for the whole list, always submitted, so
|
||||
unticking the last box still says something -- an absent checkbox
|
||||
carries no signal of its own. #}
|
||||
<input type="hidden" name="crowd_model_ids" value="" form="crowd-form">
|
||||
{% else %}
|
||||
<input type="hidden" name="crowd_model_ids" value="">
|
||||
{% endif %}
|
||||
{% for model in crowd_available %}
|
||||
<label class="picker__option picker__option--toggle">
|
||||
{% if chat %}
|
||||
{# An existing chat: written at once. The verb is on the checkbox
|
||||
and not on `#crowd-form`, because htmx binds a trigger to the
|
||||
annotated element and `change` bubbles through *ancestors* --
|
||||
which a sibling form is not. `form=` scopes the values, and
|
||||
only the values: without it the PATCH would carry the
|
||||
composer's own `content` and `project_dir`, and `update_chat`
|
||||
answers that with a 409. The same reasoning the agent mode
|
||||
select below carries. #}
|
||||
<input type="checkbox" name="crowd_model_ids" value="{{ model.model_id }}"
|
||||
{{ 'checked' if model.model_id in crowd_member_ids }}
|
||||
form="crowd-form"
|
||||
hx-patch="/api/chats/{{ chat.id }}" hx-swap="none">
|
||||
{% else %}
|
||||
<input type="checkbox" name="crowd_model_ids" value="{{ model.model_id }}"
|
||||
{{ 'checked' if model.model_id in crowd_member_ids }}>
|
||||
{% endif %}
|
||||
<span class="picker__option-body">
|
||||
<span class="picker__option-name">{{ model.label }}</span>
|
||||
{% if model.description %}
|
||||
<span class="picker__option-note">{{ model.description }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
{% if crowd_member_ids %}
|
||||
{# One sentence and not three, with the numbers as placeholders: a
|
||||
translation puts the parts in its own order, and two of these
|
||||
fragments are not sentences in any language. #}
|
||||
<p class="picker__lede">
|
||||
{{ t("%(models)s models answer each turn, over up to %(rounds)s rounds.",
|
||||
models=crowd_member_ids|length + 1, rounds=crowd_rounds) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if crowd_skipped %}
|
||||
<p class="picker__lede">
|
||||
{{ t("Skipped, because you cannot reach them any more:") }}
|
||||
<s>{{ crowd_skipped|join(", ") }}</s>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{#
|
||||
@@ -502,6 +593,12 @@
|
||||
hx-patch and not hx-post: there is no POST for a chat, only PATCH, and
|
||||
htmx shows nothing when a request 405s -- which is how these controls
|
||||
spent the first half of their lives doing nothing. #}
|
||||
{% if chat and crowd_available %}
|
||||
{# Empty, and a sibling of the composer's form rather than inside it. See the
|
||||
crowd checkboxes above, and `#agent-mode-form` below, for why both halves
|
||||
of that sentence matter. #}
|
||||
<form id="crowd-form"></form>
|
||||
{% endif %}
|
||||
{% if chat and chat.kind == "agent" %}
|
||||
<form id="agent-mode-form"></form>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user