From 3510582c8f4e1a1f0aa58adcf5dbe1a4526c295c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Bene=C5=A1?= Date: Mon, 9 Nov 2026 18:06:06 +0000 Subject: [PATCH] Subagents: one slider with two handles, Min and Max; 1 is the agent alone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chip's popover is one range from 1 to the server's cap (subagents.cap, /models subagents_cap, else 32) with two handles: two range inputs overlaid on one track, the span filled, aria-labels Min and Max with value text, keyboard as any slider, Min never past Max, the values said above it ("Min 1 · Max 4") and the two ends under it. "Fewest"/"Most" are gone. The hint says what the numbers count: the chat's own agent -- 1 is the agent alone, Max 4 is the agent and up to 3 subagents. Untouched on a new chat it still leaves the server's defaults (1-4, shown). i18n: Min, Max and the new hint in all eight languages. CHANGELOG. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 ++- scripts/gen-i18n.py | 38 ++++++++++++----- ui/app.css | 85 +++++++++++++++++++++++++++++--------- ui/i18n.js | 48 +++++++++++----------- ui/js/chat.js | 5 +-- ui/js/models.js | 99 ++++++++++++++++++++++++++++----------------- 6 files changed, 184 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9157237..6a41e15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,10 @@ All notable changes to LLeMbas Desktop. The format follows ### Changed - **Subagents**, by that name everywhere. The chip under the message box says **Subagents** with the - fewest and the most a chat runs, and opens the two fields under those words; it shows where the + least and the most agents a chat runs ("Subagents 1–4"), and opens one slider with two handles, + **Min** and **Max**, from 1 to the server's cap -- Min never passes Max, and both move with the + arrow keys as well. The numbers count the chat's own agent: **1 is the agent alone**, with no + subagents; Max 4 is the agent and up to 3 subagents. The chip shows where the server offers subagents for the chat (for a new chat, for its model), and the most is the server's cap. Where the chat's model serves one request at a time, it says that subagents need another model, on another server, named in the chat. Subagents work on the chat's own model diff --git a/scripts/gen-i18n.py b/scripts/gen-i18n.py index 3aef0f9..15aabdf 100755 --- a/scripts/gen-i18n.py +++ b/scripts/gen-i18n.py @@ -194,8 +194,8 @@ KEYS = ( "off", "default", "Think before answering", - "Fewest", - "Most", + "Min", + "Max", "Continue in a terminal…", "Continue in a terminal", "Run this in a terminal: the agent's session goes on there, in this chat's folder, and the chat keeps it.", @@ -367,7 +367,7 @@ KEYS = ( "Subagent", "Team member", "Subagents", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.", "None", "Up to %(n)s at once", @@ -382,7 +382,9 @@ KEYS = ( OWN: dict[str, dict[str, str]] = { "cs": { "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Tento model vyřizuje najednou jeden požadavek: podagenti potřebují jiný model na jiném serveru, pojmenovaný v konverzaci.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Počítá se včetně vlastního agenta konverzace. Podagent běží na modelu konverzace, na jiném jen tehdy, když ho v konverzaci pojmenujete.", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Čísla počítají i vlastního agenta konverzace: 1 je agent sám, bez podagentů; Max 4 je agent a až 3 podagenti. Podagent běží na modelu konverzace, na jiném jen tehdy, když ho v konverzaci pojmenujete.", + "Min": "Min", + "Max": "Max", "Tasks: %(n)s": "Úlohy: %(n)s", "%(n)s running": "Běží: %(n)s", "%(tokens)s until auto-compact": "%(tokens)s do automatického shrnutí", @@ -601,7 +603,9 @@ OWN: dict[str, dict[str, str]] = { }, "de": { "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Dieses Modell bedient eine Anfrage zur Zeit: Subagenten brauchen ein anderes Modell auf einem anderen Server, im Chat genannt.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Der eigene Agent des Chats zählt mit. Ein Subagent läuft auf dem Modell des Chats, auf einem anderen nur, wenn Sie es im Chat nennen.", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Die Zahlen zählen den eigenen Agenten des Chats mit: 1 ist der Agent allein, ohne Subagenten; Max 4 ist der Agent und bis zu 3 Subagenten. Ein Subagent läuft auf dem Modell des Chats, auf einem anderen nur, wenn Sie es im Chat nennen.", + "Min": "Min", + "Max": "Max", "Tasks: %(n)s": "Aufgaben: %(n)s", "%(n)s running": "%(n)s laufen", "%(tokens)s until auto-compact": "%(tokens)s bis zur automatischen Zusammenfassung", @@ -820,7 +824,9 @@ OWN: dict[str, dict[str, str]] = { }, "es": { "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Este modelo atiende una petición a la vez: los subagentes necesitan otro modelo, en otro servidor, nombrado en el chat.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Cuenta también el agente propio del chat. Un subagente usa el modelo del chat, u otro solo si lo nombra en el chat.", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Los números cuentan el agente propio del chat: 1 es el agente solo, sin subagentes; Máx. 4 es el agente y hasta 3 subagentes. Un subagente usa el modelo del chat, u otro solo si lo nombra en el chat.", + "Min": "Mín.", + "Max": "Máx.", "Tasks: %(n)s": "Tareas: %(n)s", "%(n)s running": "%(n)s en marcha", "%(tokens)s until auto-compact": "%(tokens)s hasta la compactación automática", @@ -1039,7 +1045,9 @@ OWN: dict[str, dict[str, str]] = { }, "fr": { "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ce modèle traite une requête à la fois : les sous-agents ont besoin d’un autre modèle, sur un autre serveur, nommé dans la conversation.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "L’agent de la conversation compte aussi. Un sous-agent utilise le modèle de la conversation, ou un autre seulement si vous le nommez dans la conversation.", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Les nombres comptent l’agent de la conversation : 1, c’est l’agent seul, sans sous-agents ; Max 4, c’est l’agent et jusqu’à 3 sous-agents. Un sous-agent utilise le modèle de la conversation, ou un autre seulement si vous le nommez dans la conversation.", + "Min": "Min", + "Max": "Max", "Tasks: %(n)s": "Tâches : %(n)s", "%(n)s running": "%(n)s en cours", "%(tokens)s until auto-compact": "%(tokens)s avant le compactage automatique", @@ -1258,7 +1266,9 @@ OWN: dict[str, dict[str, str]] = { }, "hu": { "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ez a modell egyszerre egy kérést szolgál ki: az alügynököknek másik modell kell, másik szerveren, a beszélgetésben megnevezve.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "A beszélgetés saját ügynöke is beleszámít. Az alügynök a beszélgetés modelljén fut, másikon csak akkor, ha megnevezi a beszélgetésben.", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "A számokba a beszélgetés saját ügynöke is beleszámít: az 1 maga az ügynök, alügynökök nélkül; a Max. 4 az ügynök és legfeljebb 3 alügynök. Az alügynök a beszélgetés modelljén fut, másikon csak akkor, ha megnevezi a beszélgetésben.", + "Min": "Min.", + "Max": "Max.", "Tasks: %(n)s": "Feladatok: %(n)s", "%(n)s running": "%(n)s fut", "%(tokens)s until auto-compact": "%(tokens)s az automatikus tömörítésig", @@ -1477,7 +1487,9 @@ OWN: dict[str, dict[str, str]] = { }, "it": { "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Questo modello serve una richiesta alla volta: i sottoagenti hanno bisogno di un altro modello, su un altro server, nominato nella chat.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Conta anche l’agente della chat. Un sottoagente usa il modello della chat, o un altro solo se lo nomini nella chat.", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "I numeri contano anche l’agente della chat: 1 è l’agente da solo, senza sottoagenti; Max 4 è l’agente più fino a 3 sottoagenti. Un sottoagente usa il modello della chat, o un altro solo se lo nomini nella chat.", + "Min": "Min", + "Max": "Max", "Tasks: %(n)s": "Attività: %(n)s", "%(n)s running": "%(n)s in corso", "%(tokens)s until auto-compact": "%(tokens)s alla compattazione automatica", @@ -1696,7 +1708,9 @@ OWN: dict[str, dict[str, str]] = { }, "pl": { "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ten model obsługuje jedno żądanie naraz: podagenci potrzebują innego modelu, na innym serwerze, wskazanego w czacie.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Liczony razem z własnym agentem czatu. Podagent działa na modelu czatu, na innym tylko wtedy, gdy wskażesz go w czacie.", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Liczby obejmują własnego agenta czatu: 1 to sam agent, bez podagentów; Maks 4 to agent i do 3 podagentów. Podagent działa na modelu czatu, na innym tylko wtedy, gdy wskażesz go w czacie.", + "Min": "Min", + "Max": "Maks", "Tasks: %(n)s": "Zadania: %(n)s", "%(n)s running": "Działa: %(n)s", "%(tokens)s until auto-compact": "%(tokens)s do automatycznego streszczenia", @@ -1915,7 +1929,9 @@ OWN: dict[str, dict[str, str]] = { }, "sk": { "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Tento model vybavuje naraz jednu požiadavku: podagenti potrebujú iný model na inom serveri, pomenovaný v konverzácii.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Počíta sa vrátane vlastného agenta konverzácie. Podagent beží na modeli konverzácie, na inom len vtedy, keď ho v konverzácii pomenujete.", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Čísla počítajú aj vlastného agenta konverzácie: 1 je agent sám, bez podagentov; Max 4 je agent a až 3 podagenti. Podagent beží na modeli konverzácie, na inom len vtedy, keď ho v konverzácii pomenujete.", + "Min": "Min", + "Max": "Max", "Tasks: %(n)s": "Úlohy: %(n)s", "%(n)s running": "Beží: %(n)s", "%(tokens)s until auto-compact": "%(tokens)s do automatického zhrnutia", diff --git a/ui/app.css b/ui/app.css index 1492b24..e68d8b7 100644 --- a/ui/app.css +++ b/ui/app.css @@ -709,28 +709,75 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); } } .toggle input:checked { background: var(--accent); border-color: var(--accent); } .toggle input:checked::after { background: var(--accent-ink); transform: translateX(var(--check-size)); } -/* The subagents, fewest and most: two number fields of one width with a dash between them, and in the header - its word before them -- fixed tracks, so nothing moves when a number grows a digit. */ -.agents, .agents__heads { - display: inline-grid; - grid-template-columns: minmax(0, 1fr) var(--sp-3) minmax(0, 1fr); - gap: var(--sp-2); - align-items: center; +/* The subagents, Min and Max: one range with two handles. Three rows of known height -- the + values said on one line, the track box (one control high), the two ends -- and in the box two + range inputs laid over one track, the span between the handles filled. The inputs let only + their handles take the pointer, so either can be grabbed wherever the other is. */ +.range2 { display: grid; grid-template-rows: auto var(--control-h) auto; gap: var(--sp-1); width: 100%; } +.range2__said { font: 600 var(--text-sm) / var(--leading-tight) var(--font-ui); color: var(--ink); font-variant-numeric: tabular-nums; } +.range2__box { position: relative; display: block; height: var(--control-h); --lo: 0%; --hi: 100%; } +.range2__track { + position: absolute; + top: 50%; + left: calc(var(--check-size) / 2); + right: calc(var(--check-size) / 2); + height: var(--sp-1); + transform: translateY(-50%); + background: var(--border); + border-radius: var(--radius-full); +} +.range2__fill { + position: absolute; + top: 0; + bottom: 0; + left: var(--lo); + right: calc(100% - var(--hi)); + background: var(--accent); + border-radius: var(--radius-full); +} +.range2__input { + position: absolute; + inset: 0; width: 100%; - height: var(--control-h); + height: 100%; + margin: 0; + background: none; + pointer-events: none; + -webkit-appearance: none; + appearance: none; } -.agents__dash { color: var(--ink-muted); text-align: center; } -/* The words over the subagents' two fields, on the fields' own tracks. */ -.agents__heads { height: auto; font-size: var(--text-xs); color: var(--ink-muted); } -.agents__input { width: 100%; min-width: 0; height: var(--control-h); padding: 0 var(--control-px-sm); } -.agents--compact { - grid-template-columns: auto calc(var(--w-field-xs) / 2) auto calc(var(--w-field-xs) / 2); - width: auto; - height: var(--control-h-sm); - font: var(--text-sm) var(--font-ui); +.range2__input.is-top { z-index: 1; } +.range2__input::-webkit-slider-runnable-track { height: 100%; background: none; border: 0; } +.range2__input::-moz-range-track { height: 100%; background: none; border: 0; } +.range2__input::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: var(--check-size); + height: var(--check-size); + margin-top: calc((var(--control-h) - var(--check-size)) / 2); + background: var(--surface-raised); + border: var(--border-w-thick) solid var(--accent); + border-radius: var(--radius-full); + box-shadow: var(--shadow-sm); + cursor: grab; + pointer-events: auto; } -.agents--compact .agents__input { height: var(--control-h-sm); } -.agents__label { color: var(--ink-muted); white-space: nowrap; } +.range2__input::-moz-range-thumb { + width: var(--check-size); + height: var(--check-size); + background: var(--surface-raised); + border: var(--border-w-thick) solid var(--accent); + border-radius: var(--radius-full); + box-shadow: var(--shadow-sm); + cursor: grab; + pointer-events: auto; +} +.range2__input:focus-visible { outline: none; } +.range2__input:focus-visible::-webkit-slider-thumb { box-shadow: var(--ring); } +.range2__input:focus-visible::-moz-range-thumb { box-shadow: var(--ring); } +/* The two ends, each one handle wide, so each number sits centred under its handle at that end. */ +.range2__ends { display: flex; justify-content: space-between; font-size: var(--text-xs); color: var(--ink-muted); font-variant-numeric: tabular-nums; } +.range2__ends > span { width: var(--check-size); text-align: center; white-space: nowrap; } /* Markdown, drawn by markdown.js from model output (never as HTML). */ .md { overflow-wrap: anywhere; min-width: 0; } .md > * + * { margin-top: var(--sp-3); } diff --git a/ui/i18n.js b/ui/i18n.js index 3203284..60e660f 100644 --- a/ui/i18n.js +++ b/ui/i18n.js @@ -83,7 +83,6 @@ const LEMBAS_I18N = { "Copied": "Zkopírováno", "Copy": "Kopírovat", "Counted by the model at the last reply; the categories are estimated.": "Spočítal model při poslední odpovědi; kategorie jsou odhadnuté.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Počítá se včetně vlastního agenta konverzace. Podagent běží na modelu konverzace, na jiném jen tehdy, když ho v konverzaci pojmenujete.", "Cut short here; the whole of it is in the web interface.": "Zde zkráceno; celé je to ve webovém rozhraní.", "Days": "Dny", "Delete": "Smazat", @@ -109,7 +108,6 @@ const LEMBAS_I18N = { "Every result, even with nothing to report.": "Každý výsledek, i když není co hlásit.", "Every weekday at 8:00, summarise the news on my reading list": "Každý pracovní den v 8:00 shrň zprávy z mého seznamu ke čtení", "Failed": "Selhalo", - "Fewest": "Nejméně", "Files": "Soubory", "Finished %(n)s": "Dokončeno: %(n)s", "Folder": "Složka", @@ -134,11 +132,13 @@ const LEMBAS_I18N = { "Looking for a LLeMbas server there…": "Hledám tam server LLeMbas…", "MCP tools": "Nástroje MCP", "Manual — asks first": "Manual — nejdřív se zeptá", + "Max": "Max", "Maximise": "Maximalizovat", "Memory and personal": "Paměť a osobní", "Menu": "Nabídka", "Message": "Zpráva", "Messages": "Zprávy", + "Min": "Min", "Minimise": "Minimalizovat", "Missed": "Zmeškáno", "Mode": "Režim", @@ -146,7 +146,6 @@ const LEMBAS_I18N = { "Models": "Modely", "Models for scheduled tasks": "Modely pro naplánované úlohy", "More": "Více", - "Most": "Nejvíce", "Never": "Nikdy", "New agent chat": "Nová agentní konverzace", "New chat": "Nová konverzace", @@ -269,6 +268,7 @@ const LEMBAS_I18N = { "The model's own": "Podle modelu", "The most an agent chat may do here": "Nejvíc, co tu agentní konverzace smí", "The next runs": "Další spuštění", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Čísla počítají i vlastního agenta konverzace: 1 je agent sám, bez podagentů; Max 4 je agent a až 3 podagenti. Podagent běží na modelu konverzace, na jiném jen tehdy, když ho v konverzaci pojmenujete.", "The reply is ready.": "Odpověď je hotová.", "The result is no longer there.": "Výsledek už tu není.", "The server refused this computer's link: %(why)s": "Server odmítl propojení tohoto počítače: %(why)s", @@ -412,7 +412,6 @@ const LEMBAS_I18N = { "Copied": "Kopiert", "Copy": "Kopieren", "Counted by the model at the last reply; the categories are estimated.": "Vom Modell bei der letzten Antwort gezählt; die Kategorien sind geschätzt.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Der eigene Agent des Chats zählt mit. Ein Subagent läuft auf dem Modell des Chats, auf einem anderen nur, wenn Sie es im Chat nennen.", "Cut short here; the whole of it is in the web interface.": "Hier gekürzt; vollständig steht es in der Weboberfläche.", "Days": "Tage", "Delete": "Löschen", @@ -438,7 +437,6 @@ const LEMBAS_I18N = { "Every result, even with nothing to report.": "Jedes Ergebnis, auch ohne Neuigkeiten.", "Every weekday at 8:00, summarise the news on my reading list": "Jeden Werktag um 8:00 die Nachrichten auf meiner Leseliste zusammenfassen", "Failed": "Fehlgeschlagen", - "Fewest": "Mindestens", "Files": "Dateien", "Finished %(n)s": "Fertig: %(n)s", "Folder": "Ordner", @@ -463,11 +461,13 @@ const LEMBAS_I18N = { "Looking for a LLeMbas server there…": "Suche dort nach einem LLeMbas-Server…", "MCP tools": "MCP-Werkzeuge", "Manual — asks first": "Manual — fragt zuerst", + "Max": "Max", "Maximise": "Maximieren", "Memory and personal": "Gedächtnis und Persönliches", "Menu": "Menü", "Message": "Nachricht", "Messages": "Nachrichten", + "Min": "Min", "Minimise": "Minimieren", "Missed": "Verpasst", "Mode": "Modus", @@ -475,7 +475,6 @@ const LEMBAS_I18N = { "Models": "Modelle", "Models for scheduled tasks": "Modelle für geplante Aufgaben", "More": "Mehr", - "Most": "Höchstens", "Never": "Nie", "New agent chat": "Neuer Agenten-Chat", "New chat": "Neuer Chat", @@ -598,6 +597,7 @@ const LEMBAS_I18N = { "The model's own": "Die des Modells", "The most an agent chat may do here": "Das Meiste, was ein Agent-Chat hier darf", "The next runs": "Die nächsten Läufe", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Die Zahlen zählen den eigenen Agenten des Chats mit: 1 ist der Agent allein, ohne Subagenten; Max 4 ist der Agent und bis zu 3 Subagenten. Ein Subagent läuft auf dem Modell des Chats, auf einem anderen nur, wenn Sie es im Chat nennen.", "The reply is ready.": "Die Antwort ist fertig.", "The result is no longer there.": "Das Ergebnis ist nicht mehr da.", "The server refused this computer's link: %(why)s": "Der Server hat die Verknüpfung dieses Computers abgelehnt: %(why)s", @@ -741,7 +741,6 @@ const LEMBAS_I18N = { "Copied": "Copiado", "Copy": "Copiar", "Counted by the model at the last reply; the categories are estimated.": "Contado por el modelo en la última respuesta; las categorías son estimaciones.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Cuenta también el agente propio del chat. Un subagente usa el modelo del chat, u otro solo si lo nombra en el chat.", "Cut short here; the whole of it is in the web interface.": "Recortado aquí; completo está en la interfaz web.", "Days": "Días", "Delete": "Eliminar", @@ -767,7 +766,6 @@ const LEMBAS_I18N = { "Every result, even with nothing to report.": "Cada resultado, incluso sin nada que informar.", "Every weekday at 8:00, summarise the news on my reading list": "Cada día laborable a las 8:00, resume las noticias de mi lista de lectura", "Failed": "Fallidos", - "Fewest": "Mínimo", "Files": "Archivos", "Finished %(n)s": "Terminadas: %(n)s", "Folder": "Carpeta", @@ -792,11 +790,13 @@ const LEMBAS_I18N = { "Looking for a LLeMbas server there…": "Buscando un servidor LLeMbas allí…", "MCP tools": "Herramientas MCP", "Manual — asks first": "Manual — pregunta primero", + "Max": "Máx.", "Maximise": "Maximizar", "Memory and personal": "Memoria y datos personales", "Menu": "Menú", "Message": "Mensaje", "Messages": "Mensajes", + "Min": "Mín.", "Minimise": "Minimizar", "Missed": "Perdida", "Mode": "Modo", @@ -804,7 +804,6 @@ const LEMBAS_I18N = { "Models": "Modelos", "Models for scheduled tasks": "Modelos para tareas programadas", "More": "Más", - "Most": "Máximo", "Never": "Nunca", "New agent chat": "Nuevo chat de agente", "New chat": "Nuevo chat", @@ -927,6 +926,7 @@ const LEMBAS_I18N = { "The model's own": "La del modelo", "The most an agent chat may do here": "Lo máximo que un chat de agente puede hacer aquí", "The next runs": "Las próximas ejecuciones", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Los números cuentan el agente propio del chat: 1 es el agente solo, sin subagentes; Máx. 4 es el agente y hasta 3 subagentes. Un subagente usa el modelo del chat, u otro solo si lo nombra en el chat.", "The reply is ready.": "La respuesta está lista.", "The result is no longer there.": "El resultado ya no está.", "The server refused this computer's link: %(why)s": "El servidor rechazó la vinculación de este equipo: %(why)s", @@ -1070,7 +1070,6 @@ const LEMBAS_I18N = { "Copied": "Copié", "Copy": "Copier", "Counted by the model at the last reply; the categories are estimated.": "Compté par le modèle lors de la dernière réponse ; les catégories sont estimées.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "L’agent de la conversation compte aussi. Un sous-agent utilise le modèle de la conversation, ou un autre seulement si vous le nommez dans la conversation.", "Cut short here; the whole of it is in the web interface.": "Tronqué ici ; le tout est dans l'interface web.", "Days": "Jours", "Delete": "Supprimer", @@ -1096,7 +1095,6 @@ const LEMBAS_I18N = { "Every result, even with nothing to report.": "Chaque résultat, même sans rien à signaler.", "Every weekday at 8:00, summarise the news on my reading list": "Chaque jour ouvré à 8 h, résume les nouvelles de ma liste de lecture", "Failed": "Échecs", - "Fewest": "Au moins", "Files": "Fichiers", "Finished %(n)s": "Terminées : %(n)s", "Folder": "Dossier", @@ -1121,11 +1119,13 @@ const LEMBAS_I18N = { "Looking for a LLeMbas server there…": "Recherche d'un serveur LLeMbas à cette adresse…", "MCP tools": "Outils MCP", "Manual — asks first": "Manual — demande d'abord", + "Max": "Max", "Maximise": "Agrandir", "Memory and personal": "Mémoire et informations personnelles", "Menu": "Menu", "Message": "Message", "Messages": "Messages", + "Min": "Min", "Minimise": "Réduire", "Missed": "Manquée", "Mode": "Mode", @@ -1133,7 +1133,6 @@ const LEMBAS_I18N = { "Models": "Modèles", "Models for scheduled tasks": "Modèles pour les tâches planifiées", "More": "Plus", - "Most": "Au plus", "Never": "Jamais", "New agent chat": "Nouvelle discussion d'agent", "New chat": "Nouvelle conversation", @@ -1256,6 +1255,7 @@ const LEMBAS_I18N = { "The model's own": "Celle du modèle", "The most an agent chat may do here": "Le maximum qu'une conversation d'agent peut faire ici", "The next runs": "Les prochaines exécutions", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Les nombres comptent l’agent de la conversation : 1, c’est l’agent seul, sans sous-agents ; Max 4, c’est l’agent et jusqu’à 3 sous-agents. Un sous-agent utilise le modèle de la conversation, ou un autre seulement si vous le nommez dans la conversation.", "The reply is ready.": "La réponse est prête.", "The result is no longer there.": "Le résultat n'est plus là.", "The server refused this computer's link: %(why)s": "Le serveur a refusé la liaison de cet ordinateur : %(why)s", @@ -1399,7 +1399,6 @@ const LEMBAS_I18N = { "Copied": "Másolva", "Copy": "Másolás", "Counted by the model at the last reply; the categories are estimated.": "A modell számolta az utolsó válasznál; a kategóriák becslések.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "A beszélgetés saját ügynöke is beleszámít. Az alügynök a beszélgetés modelljén fut, másikon csak akkor, ha megnevezi a beszélgetésben.", "Cut short here; the whole of it is in the web interface.": "Itt rövidítve; a teljes a webes felületen van.", "Days": "Napok", "Delete": "Törlés", @@ -1425,7 +1424,6 @@ const LEMBAS_I18N = { "Every result, even with nothing to report.": "Minden eredmény, akkor is, ha nincs mit jelenteni.", "Every weekday at 8:00, summarise the news on my reading list": "Minden hétköznap 8:00-kor foglald össze az olvasólistám híreit", "Failed": "Sikertelen", - "Fewest": "Legalább", "Files": "Fájlok", "Finished %(n)s": "Kész: %(n)s", "Folder": "Mappa", @@ -1450,11 +1448,13 @@ const LEMBAS_I18N = { "Looking for a LLeMbas server there…": "LLeMbas-kiszolgáló keresése ott…", "MCP tools": "MCP-eszközök", "Manual — asks first": "Manual — előbb kérdez", + "Max": "Max.", "Maximise": "Teljes méret", "Memory and personal": "Memória és személyes adatok", "Menu": "Menü", "Message": "Üzenet", "Messages": "Üzenetek", + "Min": "Min.", "Minimise": "Kis méret", "Missed": "Elmaradt", "Mode": "Mód", @@ -1462,7 +1462,6 @@ const LEMBAS_I18N = { "Models": "Modellek", "Models for scheduled tasks": "Modellek az ütemezett feladatokhoz", "More": "Több", - "Most": "Legfeljebb", "Never": "Soha", "New agent chat": "Új ügynökcsevegés", "New chat": "Új csevegés", @@ -1585,6 +1584,7 @@ const LEMBAS_I18N = { "The model's own": "A modell sajátja", "The most an agent chat may do here": "A legtöbb, amit egy ügynök-csevegés itt tehet", "The next runs": "A következő futások", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "A számokba a beszélgetés saját ügynöke is beleszámít: az 1 maga az ügynök, alügynökök nélkül; a Max. 4 az ügynök és legfeljebb 3 alügynök. Az alügynök a beszélgetés modelljén fut, másikon csak akkor, ha megnevezi a beszélgetésben.", "The reply is ready.": "A válasz kész.", "The result is no longer there.": "Az eredmény már nincs meg.", "The server refused this computer's link: %(why)s": "A szerver elutasította ennek a számítógépnek a kapcsolatát: %(why)s", @@ -1728,7 +1728,6 @@ const LEMBAS_I18N = { "Copied": "Copiato", "Copy": "Copia", "Counted by the model at the last reply; the categories are estimated.": "Contato dal modello all’ultima risposta; le categorie sono stime.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Conta anche l’agente della chat. Un sottoagente usa il modello della chat, o un altro solo se lo nomini nella chat.", "Cut short here; the whole of it is in the web interface.": "Troncato qui; il resto è nell'interfaccia web.", "Days": "Giorni", "Delete": "Elimina", @@ -1754,7 +1753,6 @@ const LEMBAS_I18N = { "Every result, even with nothing to report.": "Ogni risultato, anche senza nulla da segnalare.", "Every weekday at 8:00, summarise the news on my reading list": "Ogni giorno feriale alle 8:00, riassumi le notizie della mia lista di lettura", "Failed": "Non riusciti", - "Fewest": "Minimo", "Files": "File", "Finished %(n)s": "Terminate: %(n)s", "Folder": "Cartella", @@ -1779,11 +1777,13 @@ const LEMBAS_I18N = { "Looking for a LLeMbas server there…": "Cerco un server LLeMbas lì…", "MCP tools": "Strumenti MCP", "Manual — asks first": "Manual — chiede prima", + "Max": "Max", "Maximise": "Ingrandisci", "Memory and personal": "Memoria e dati personali", "Menu": "Menu", "Message": "Messaggio", "Messages": "Messaggi", + "Min": "Min", "Minimise": "Riduci a icona", "Missed": "Mancata", "Mode": "Modalità", @@ -1791,7 +1791,6 @@ const LEMBAS_I18N = { "Models": "Modelli", "Models for scheduled tasks": "Modelli per le attività pianificate", "More": "Altro", - "Most": "Massimo", "Never": "Mai", "New agent chat": "Nuova chat con agente", "New chat": "Nuova chat", @@ -1914,6 +1913,7 @@ const LEMBAS_I18N = { "The model's own": "Quello del modello", "The most an agent chat may do here": "Il massimo che una chat agente può fare qui", "The next runs": "Le prossime esecuzioni", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "I numeri contano anche l’agente della chat: 1 è l’agente da solo, senza sottoagenti; Max 4 è l’agente più fino a 3 sottoagenti. Un sottoagente usa il modello della chat, o un altro solo se lo nomini nella chat.", "The reply is ready.": "La risposta è pronta.", "The result is no longer there.": "Il risultato non c’è più.", "The server refused this computer's link: %(why)s": "Il server ha rifiutato il collegamento di questo computer: %(why)s", @@ -2057,7 +2057,6 @@ const LEMBAS_I18N = { "Copied": "Skopiowano", "Copy": "Kopiuj", "Counted by the model at the last reply; the categories are estimated.": "Policzone przez model przy ostatniej odpowiedzi; kategorie są szacunkowe.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Liczony razem z własnym agentem czatu. Podagent działa na modelu czatu, na innym tylko wtedy, gdy wskażesz go w czacie.", "Cut short here; the whole of it is in the web interface.": "Tu skrócone; całość jest w interfejsie WWW.", "Days": "Dni", "Delete": "Usuń", @@ -2083,7 +2082,6 @@ const LEMBAS_I18N = { "Every result, even with nothing to report.": "Każdy wynik, nawet bez niczego do zgłoszenia.", "Every weekday at 8:00, summarise the news on my reading list": "W każdy dzień roboczy o 8:00 streść wiadomości z mojej listy do czytania", "Failed": "Nieudane", - "Fewest": "Najmniej", "Files": "Pliki", "Finished %(n)s": "Zakończone: %(n)s", "Folder": "Folder", @@ -2108,11 +2106,13 @@ const LEMBAS_I18N = { "Looking for a LLeMbas server there…": "Szukam tam serwera LLeMbas…", "MCP tools": "Narzędzia MCP", "Manual — asks first": "Manual — najpierw pyta", + "Max": "Maks", "Maximise": "Maksymalizuj", "Memory and personal": "Pamięć i dane osobiste", "Menu": "Menu", "Message": "Wiadomość", "Messages": "Wiadomości", + "Min": "Min", "Minimise": "Minimalizuj", "Missed": "Przegapiony", "Mode": "Tryb", @@ -2120,7 +2120,6 @@ const LEMBAS_I18N = { "Models": "Modele", "Models for scheduled tasks": "Modele dla zaplanowanych zadań", "More": "Więcej", - "Most": "Najwięcej", "Never": "Nigdy", "New agent chat": "Nowy czat z agentem", "New chat": "Nowy czat", @@ -2243,6 +2242,7 @@ const LEMBAS_I18N = { "The model's own": "Domyślne modelu", "The most an agent chat may do here": "Najwięcej, co czat agenta może tu zrobić", "The next runs": "Następne uruchomienia", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Liczby obejmują własnego agenta czatu: 1 to sam agent, bez podagentów; Maks 4 to agent i do 3 podagentów. Podagent działa na modelu czatu, na innym tylko wtedy, gdy wskażesz go w czacie.", "The reply is ready.": "Odpowiedź jest gotowa.", "The result is no longer there.": "Wyniku już nie ma.", "The server refused this computer's link: %(why)s": "Serwer odrzucił połączenie tego komputera: %(why)s", @@ -2386,7 +2386,6 @@ const LEMBAS_I18N = { "Copied": "Skopírované", "Copy": "Kopírovať", "Counted by the model at the last reply; the categories are estimated.": "Spočítal model pri poslednej odpovedi; kategórie sú odhadnuté.", - "Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Počíta sa vrátane vlastného agenta konverzácie. Podagent beží na modeli konverzácie, na inom len vtedy, keď ho v konverzácii pomenujete.", "Cut short here; the whole of it is in the web interface.": "Tu skrátené; celé je to vo webovom rozhraní.", "Days": "Dni", "Delete": "Zmazať", @@ -2412,7 +2411,6 @@ const LEMBAS_I18N = { "Every result, even with nothing to report.": "Každý výsledok, aj keď nie je čo hlásiť.", "Every weekday at 8:00, summarise the news on my reading list": "Každý pracovný deň o 8:00 zhrň správy z môjho zoznamu na čítanie", "Failed": "Zlyhalo", - "Fewest": "Najmenej", "Files": "Súbory", "Finished %(n)s": "Dokončené: %(n)s", "Folder": "Priečinok", @@ -2437,11 +2435,13 @@ const LEMBAS_I18N = { "Looking for a LLeMbas server there…": "Hľadám tam server LLeMbas…", "MCP tools": "Nástroje MCP", "Manual — asks first": "Manual — najprv sa opýta", + "Max": "Max", "Maximise": "Maximalizovať", "Memory and personal": "Pamäť a osobné", "Menu": "Ponuka", "Message": "Správa", "Messages": "Správy", + "Min": "Min", "Minimise": "Minimalizovať", "Missed": "Zmeškané", "Mode": "Režim", @@ -2449,7 +2449,6 @@ const LEMBAS_I18N = { "Models": "Modely", "Models for scheduled tasks": "Modely pre naplánované úlohy", "More": "Viac", - "Most": "Najviac", "Never": "Nikdy", "New agent chat": "Nová agentská konverzácia", "New chat": "Nová konverzácia", @@ -2572,6 +2571,7 @@ const LEMBAS_I18N = { "The model's own": "Podľa modelu", "The most an agent chat may do here": "Najviac, čo tu agentská konverzácia smie", "The next runs": "Ďalšie spustenia", + "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Čísla počítajú aj vlastného agenta konverzácie: 1 je agent sám, bez podagentov; Max 4 je agent a až 3 podagenti. Podagent beží na modeli konverzácie, na inom len vtedy, keď ho v konverzácii pomenujete.", "The reply is ready.": "Odpoveď je hotová.", "The result is no longer there.": "Výsledok už tu nie je.", "The server refused this computer's link: %(why)s": "Server odmietol prepojenie tohto počítača: %(why)s", diff --git a/ui/js/chat.js b/ui/js/chat.js index 44e75cd..ef54e70 100644 --- a/ui/js/chat.js +++ b/ui/js/chat.js @@ -652,7 +652,7 @@ function effortChip(model, value, onChange) { } /** - * The subagents a chat may run: a chip saying the fewest and the most, opening the two fields. + * The subagents a chat may run: a chip saying Min–Max, opening one range with two handles. * `value` { min, max } or empty; `cap` the instance's most, where the server says it. */ function agentsChip(value, onChange, { cap, own } = {}) { @@ -669,8 +669,7 @@ function agentsChip(value, onChange, { cap, own } = {}) { h( "div", { class: "pop__body" }, - h("label", { class: "pop__title", for: "agents-min", text: t("Subagents") }), - h("div", { class: "agents__heads", "aria-hidden": "true" }, h("span", { text: t("Fewest") }), h("span"), h("span", { text: t("Most") })), + h("p", { class: "pop__title", text: t("Subagents") }), fields, own ? h("p", { class: "hint is-warn", text: SUBAGENTS_OWN() }) : null, h("p", { class: "hint", text: SUBAGENTS_HINT() }), diff --git a/ui/js/models.js b/ui/js/models.js index 903b866..f4b3ae9 100644 --- a/ui/js/models.js +++ b/ui/js/models.js @@ -226,64 +226,87 @@ export function subagentsCap(...sources) { } return knownCap || SUBAGENTS_CAP } -export const SUBAGENTS_HINT = () => t("Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.") +// The numbers count the chat's own agent: 1 is the agent alone. +export const SUBAGENTS_HINT = () => t("The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.") // The spec's `subagents.setting.own`: beside the setting where the chat's model cannot serve them. export const SUBAGENTS_OWN = () => t("This model serves one request at a time: subagents need another model, on another server, named in the chat.") +// A new chat's setting as the harness spec seeds it (`loop.json` agents): 1 to 4. +export const AGENTS_DEFAULT = { min: 1, max: 4 } + /** - * The subagents a chat may run: the fewest and the most, as two number fields on one shared - * track. `value` is { min, max } (empty fields for a new chat: the server's defaults then stand); - * `cap` the instance's most; `onChange({ min, max })` once a field is changed, with the empty one - * left out. The server checks the pair and says the bounds when it refuses one. + * The agents a chat may run, its own agent counted: one range from 1 to `cap` with two handles, + * Min and Max -- two range inputs overlaid on one track, the span between them filled. Each is a + * real slider (arrow keys, Home/End, Page Up/Down), and Min never passes Max. `value` is + * { min, max } (empty for a new chat: the spec's defaults are shown, and left to the server + * unless a handle moves); `el.value` the pair. The server checks it again. */ -export function agentsControl({ value, onChange, id, compact, cap = SUBAGENTS_CAP }) { - const field = (which, v, label) => +export function agentsControl({ value, onChange, id, cap = SUBAGENTS_CAP }) { + const top = Math.max(1, Number(cap) || SUBAGENTS_CAP) + const clamp = (n, d) => Math.min(top, Math.max(1, Number.isFinite(Number(n)) && n !== null && n !== "" ? Number(n) : d)) + let given = value || {} + let touched = false + const handle = (which, label) => h("input", { - type: "number", - class: "agents__input", + type: "range", + class: `range2__input range2__input--${which}`, id: which === "min" ? id : null, min: "1", - max: String(cap), + max: String(top), step: "1", - inputmode: "numeric", - value: v === undefined || v === null ? "" : String(v), - placeholder: compact ? null : t("default"), "aria-label": label, title: label, }) - const low = field("min", value?.min, t("Fewest")) - const high = field("max", value?.max, t("Most")) - const bounds = () => { - high.min = low.value || "1" - low.max = high.value || String(cap) + const low = handle("min", t("Min")) + const high = handle("max", t("Max")) + const said = h("output", { class: "range2__said", "aria-live": "polite", for: id || null }) + const fill = h("span", { class: "range2__fill", "aria-hidden": "true" }) + const box = h("span", { class: "range2__box" }, h("span", { class: "range2__track", "aria-hidden": "true" }, fill), low, high) + const draw = () => { + const lo = Number(low.value) + const hi = Number(high.value) + const at = (n) => (top > 1 ? ((n - 1) / (top - 1)) * 100 : 0) + box.style.setProperty("--lo", `${at(lo)}%`) + box.style.setProperty("--hi", `${at(hi)}%`) + // Where the two meet, the one that can still move is on top: Min at the far end, else Max. + low.classList.toggle("is-top", lo === hi && hi === top) + low.setAttribute("aria-valuetext", `${t("Min")} ${lo}`) + high.setAttribute("aria-valuetext", `${t("Max")} ${hi}`) + said.textContent = `${t("Min")} ${lo} · ${t("Max")} ${hi}` } - bounds() - const read = () => { - const out = {} - if (low.value) out.min = Number(low.value) - if (high.value) out.max = Number(high.value) - return out - } - for (const input of [low, high]) { - input.addEventListener("change", () => { - bounds() - onChange?.(read()) - }) + const set = (v) => { + const hi = clamp(v?.max, Math.min(AGENTS_DEFAULT.max, top)) + const lo = Math.min(hi, clamp(v?.min, AGENTS_DEFAULT.min)) + high.value = String(hi) + low.value = String(lo) + draw() } + low.addEventListener("input", () => { + if (Number(low.value) > Number(high.value)) low.value = high.value + touched = true + draw() + }) + high.addEventListener("input", () => { + if (Number(high.value) < Number(low.value)) high.value = low.value + touched = true + draw() + }) + for (const input of [low, high]) input.addEventListener("change", () => onChange?.(read())) + const read = () => (touched || given.min || given.max ? { min: Number(low.value), max: Number(high.value) } : {}) + set(given) const el = h( "span", - { class: `agents${compact ? " agents--compact" : ""}`, role: "group", "aria-label": t("Subagents"), title: SUBAGENTS_HINT() }, - compact ? h("span", { class: "agents__label", text: t("Subagents") }) : null, - low, - h("span", { class: "agents__dash", "aria-hidden": "true", text: "–" }), - high, + { class: "range2", role: "group", "aria-label": t("Subagents") }, + said, + box, + h("span", { class: "range2__ends", "aria-hidden": "true" }, h("span", { text: "1" }), h("span", { text: String(top) })), ) Object.defineProperty(el, "value", { get: read, set: (v) => { - low.value = v?.min ?? "" - high.value = v?.max ?? "" - bounds() + given = v || {} + touched = false + set(given) }, }) return el