2 Commits
Author SHA1 Message Date
HomerandClaude Opus 5.5 ede91887f7 Subagents and Compact on the server's /api/v2 facts (server 2.0.0-beta.7)
ci / check (push) Failing after 3s
ci / start (push) Failing after 3s
- Models' `subagents {offered, named_only}` and /models' `subagents_cap`; a chat's
  `subagents {offered, named_only, cap}` decides the chip for an open chat (an agent chat
  gets none where the server says so); `named_only` shows the spec's `own` line in the
  popover and marks the chip. GET /chats/{chat}/team stays the fallback for older servers.
- Context: `auto_compact`, `until_compact` and `compactable` from the server (worked out from
  `threshold` with an older one); Compact greyed while not compactable.
- "Tasks: N" for a task's nested fold, the server's sentence; the subagent words aligned with
  the server's catalogues (Podagenti, Sottoagenti, Podagenci); hint says the same-model rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 17:15:38 +00:00
HomerandClaude Opus 5.5 6bad293b29 Subagents by name; the activity panel as the reference: header button, compact, tasks
ci / check (push) Failing after 3s
ci / start (push) Failing after 4s
- "Subagents" everywhere a person reads it (chip, popover with Fewest/Most, scheduled task,
  panel); offered per the server's `subagents` on a model or chat where it says it, else as
  before; the fields' most is the server's cap, else 32. No subagent-model setting.
- Panel: always reachable from the header's button; the line above the composer only while
  something runs; Environment for an agent chat; Context with "N until auto-compact" and a
  Compact button (POST /chats/{chat}/compact; a server without it is said so); Background
  tasks under Running with elapsed time and a stop square each, unfolding to kind, step,
  live text and conversation, nested tasks folded under their parent, Finished N folded;
  Room and Plan as sections.
- i18n: 24 new sentences x 8 languages, 9 retired; CHANGELOG [Unreleased].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 16:55:09 +00:00
10 changed files with 802 additions and 237 deletions

No files matched your search

+20
View File
@@ -6,6 +6,26 @@ All notable changes to LLeMbas Desktop. The format follows
## [Unreleased]
### 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
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
unless you name another one in the chat -- there is no setting for a subagent's model. A
scheduled task's field is **Subagents** too. Needs LLeMbas 2.0.0-beta.7 for the server's word on
all of this; with an older one the app guesses as before.
- **The activity panel, reshaped.** A button in the chat's header opens it at any time, and the
line above the message box shows only while something runs ("3 running · 1 queued · 2
finished"). An agent chat's **Environment** (its folder, and where it is) comes first. **Context**
says how far it is to the automatic compaction ("80.6k until auto-compact") beside a **Compact**
button that summarises the earlier turns now (greyed while there is nothing to summarise). **Background tasks** lists subagents, a team's
members and background commands under **Running**, each with how long it has run and a stop
square, unfolding to what it is, its latest step, what it is writing and its conversation; the
tasks under one fold beneath it ("Tasks: 2"), and the finished ones fold away under **Finished**.
The team's **Room** and the **Plan** have sections of their own.
## [1.0.0-beta.4] — 2026-11-09
Carries LLeMbas CLI 2.0.0-beta.3 and the look of LLeMbas 2.0.0-beta.6. Scheduled tasks need LLeMbas
+217 -33
View File
@@ -196,8 +196,6 @@ KEYS = (
"Think before answering",
"Fewest",
"Most",
"Agents",
"Its own agent included. Above one, it can add members that do the work while it leads them.",
"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.",
@@ -207,8 +205,6 @@ KEYS = (
"Back",
"Open the activity panel",
"Close the activity panel",
"1 agent working",
"%(n)s agents working",
"%(n)s queued",
"%(n)s waiting",
"%(n)s finished",
@@ -240,9 +236,7 @@ KEYS = (
"Estimated: no reply has been counted yet.",
"%(n)s tools offered.",
"No files written in this chat yet.",
"Nothing runs in the background. Helpers, commands and a team's members show here while they work.",
"Nothing running.",
"Team: %(size)s of %(max)s agents",
"Show %(n)s more",
"Room",
"report",
@@ -323,8 +317,6 @@ KEYS = (
"A busy model is waited for, then the next free one is used.",
"A result, but not a run with nothing to report.",
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.",
"A team of up to %(n)s",
"A team works on it together, its own agent included.",
"Choose a model to choose its effort.",
"Due %(when)s",
"Each run sees the ones before it.",
@@ -333,7 +325,6 @@ KEYS = (
"Edit %(title)s",
"Every result, even with nothing to report.",
"Models for scheduled tasks",
"No team",
"Only in its history.",
"Open the chat",
"Result",
@@ -358,6 +349,31 @@ KEYS = (
"Open its chat",
"The model's own",
"run by hand",
# activity.js, chat.js, models.js, scheduled.js: subagents, the panel, Compact
"%(n)s running",
"Tasks: %(n)s",
"%(tokens)s until auto-compact",
"Automatic compaction is off.",
"Compacts before the next reply.",
"Compacts by itself once the window is known.",
"Compact",
"Compacting…",
"Summarise the earlier turns now and send the summary in their place.",
"This server cannot compact a chat from the app yet; the web interface can.",
"Context and background tasks",
"Environment",
"Finished %(n)s",
"Background command",
"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.",
"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",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.",
"Team: %(size)s of at most %(max)s subagents",
"How many work on it at once, the run's own agent among them, on the same model.",
)
# The app's own sentences. The mode names (Manual, Edit, Auto, Plan) stay English, as the
@@ -365,6 +381,30 @@ KEYS = (
# the device, as the server's catalogue has it.
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.",
"Tasks: %(n)s": "Úlohy: %(n)s",
"%(n)s running": "Běží: %(n)s",
"%(tokens)s until auto-compact": "%(tokens)s do automatického shrnutí",
"Automatic compaction is off.": "Automatické shrnutí je vypnuté.",
"Compacts before the next reply.": "Shrne se před další odpovědí.",
"Compacts by itself once the window is known.": "Shrne se samo, jakmile bude známé okno.",
"Compact": "Shrnout",
"Compacting…": "Shrnuje se…",
"Summarise the earlier turns now and send the summary in their place.": "Shrnout dřívější kola hned a posílat místo nich shrnutí.",
"This server cannot compact a chat from the app yet; the web interface can.": "Tento server zatím neumí shrnout konverzaci z aplikace; webové rozhraní to umí.",
"Context and background tasks": "Kontext a úlohy na pozadí",
"Environment": "Prostředí",
"Finished %(n)s": "Dokončeno: %(n)s",
"Background command": "Příkaz na pozadí",
"Subagent": "Podagent",
"Team member": "Člen týmu",
"Subagents": "Podagenti",
"None": "Žádní",
"Up to %(n)s at once": "Nejvýše %(n)s současně",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Na pozadí nic neběží. Podagenti, členové týmu a příkazy na pozadí se tu zobrazí, dokud pracují.",
"Team: %(size)s of at most %(max)s subagents": "Tým: %(size)s z nejvýše %(max)s podagentů",
"How many work on it at once, the run's own agent among them, on the same model.": "Kolik jich na ní pracuje současně, včetně vlastního agenta běhu, na stejném modelu.",
"Menu": "Nabídka",
"Minimise": "Minimalizovat",
"Maximise": "Maximalizovat",
@@ -532,8 +572,6 @@ OWN: dict[str, dict[str, str]] = {
"A busy model is waited for, then the next free one is used.": "Na zaneprázdněný model se čeká, pak se použije další volný.",
"A result, but not a run with nothing to report.": "Výsledek, ale ne spuštění, které nemá co hlásit.",
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Úloha bez vlastního modelu běží na prvním volném z těchto, v tomto pořadí. Žádný nezaškrtnutý: všechny.",
"A team of up to %(n)s": "Tým: nejvýše %(n)s",
"A team works on it together, its own agent included.": "Pracuje na ní tým společně, včetně jejího vlastního agenta.",
"Choose a model to choose its effort.": "Vyberte model, abyste mohli zvolit jeho náročnost.",
"Due %(when)s": "Naplánováno na %(when)s",
"Each run sees the ones before it.": "Každé spuštění vidí ta předchozí.",
@@ -542,7 +580,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "Upravit %(title)s",
"Every result, even with nothing to report.": "Každý výsledek, i když není co hlásit.",
"Models for scheduled tasks": "Modely pro naplánované úlohy",
"No team": "Bez týmu",
"Only in its history.": "Jen v její historii.",
"Open the chat": "Otevřít konverzaci",
"Result": "Výsledek",
@@ -563,6 +600,30 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Žádný den nezaškrtnutý: každý den.",
},
"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.",
"Tasks: %(n)s": "Aufgaben: %(n)s",
"%(n)s running": "%(n)s laufen",
"%(tokens)s until auto-compact": "%(tokens)s bis zur automatischen Zusammenfassung",
"Automatic compaction is off.": "Die automatische Zusammenfassung ist aus.",
"Compacts before the next reply.": "Wird vor der nächsten Antwort zusammengefasst.",
"Compacts by itself once the window is known.": "Wird von selbst zusammengefasst, sobald das Fenster bekannt ist.",
"Compact": "Zusammenfassen",
"Compacting…": "Wird zusammengefasst…",
"Summarise the earlier turns now and send the summary in their place.": "Die früheren Runden jetzt zusammenfassen und stattdessen die Zusammenfassung senden.",
"This server cannot compact a chat from the app yet; the web interface can.": "Dieser Server kann einen Chat noch nicht aus der App zusammenfassen; die Weboberfläche kann es.",
"Context and background tasks": "Kontext und Hintergrundaufgaben",
"Environment": "Umgebung",
"Finished %(n)s": "Fertig: %(n)s",
"Background command": "Hintergrundbefehl",
"Subagent": "Subagent",
"Team member": "Teammitglied",
"Subagents": "Subagenten",
"None": "Keine",
"Up to %(n)s at once": "Bis zu %(n)s gleichzeitig",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Im Hintergrund läuft nichts. Subagenten, die Mitglieder eines Teams und Hintergrundbefehle erscheinen hier, solange sie arbeiten.",
"Team: %(size)s of at most %(max)s subagents": "Team: %(size)s von höchstens %(max)s Subagenten",
"How many work on it at once, the run's own agent among them, on the same model.": "Wie viele gleichzeitig daran arbeiten, der eigene Agent des Laufs mitgezählt, mit demselben Modell.",
"Menu": "Menü",
"Minimise": "Minimieren",
"Maximise": "Maximieren",
@@ -730,8 +791,6 @@ OWN: dict[str, dict[str, str]] = {
"A busy model is waited for, then the next free one is used.": "Auf ein belegtes Modell wird gewartet, dann wird das nächste freie genommen.",
"A result, but not a run with nothing to report.": "Ein Ergebnis, aber kein Lauf ohne Neuigkeiten.",
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Eine Aufgabe ohne eigenes Modell läuft auf dem ersten freien davon, in dieser Reihenfolge. Keines angehakt: alle.",
"A team of up to %(n)s": "Ein Team mit bis zu %(n)s",
"A team works on it together, its own agent included.": "Ein Team arbeitet gemeinsam daran, ihr eigener Agent eingeschlossen.",
"Choose a model to choose its effort.": "Wählen Sie ein Modell, um seinen Aufwand zu wählen.",
"Due %(when)s": "Fällig %(when)s",
"Each run sees the ones before it.": "Jeder Lauf sieht die vorherigen.",
@@ -740,7 +799,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "%(title)s bearbeiten",
"Every result, even with nothing to report.": "Jedes Ergebnis, auch ohne Neuigkeiten.",
"Models for scheduled tasks": "Modelle für geplante Aufgaben",
"No team": "Kein Team",
"Only in its history.": "Nur in ihrem Verlauf.",
"Open the chat": "Chat öffnen",
"Result": "Ergebnis",
@@ -761,6 +819,30 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Kein Tag angehakt: jeden Tag.",
},
"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.",
"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",
"Automatic compaction is off.": "La compactación automática está desactivada.",
"Compacts before the next reply.": "Se compacta antes de la próxima respuesta.",
"Compacts by itself once the window is known.": "Se compacta sola en cuanto se conozca la ventana.",
"Compact": "Compactar",
"Compacting…": "Compactando…",
"Summarise the earlier turns now and send the summary in their place.": "Resumir ahora los turnos anteriores y enviar el resumen en su lugar.",
"This server cannot compact a chat from the app yet; the web interface can.": "Este servidor aún no puede compactar un chat desde la aplicación; la interfaz web sí puede.",
"Context and background tasks": "Contexto y tareas en segundo plano",
"Environment": "Entorno",
"Finished %(n)s": "Terminadas: %(n)s",
"Background command": "Comando en segundo plano",
"Subagent": "Subagente",
"Team member": "Miembro del equipo",
"Subagents": "Subagentes",
"None": "Ninguno",
"Up to %(n)s at once": "Hasta %(n)s a la vez",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "No hay nada en segundo plano. Los subagentes, los miembros de un equipo y los comandos en segundo plano aparecen aquí mientras trabajan.",
"Team: %(size)s of at most %(max)s subagents": "Equipo: %(size)s de un máximo de %(max)s subagentes",
"How many work on it at once, the run's own agent among them, on the same model.": "Cuántos trabajan en ella a la vez, contando el propio agente de la ejecución, con el mismo modelo.",
"Menu": "Menú",
"Minimise": "Minimizar",
"Maximise": "Maximizar",
@@ -928,8 +1010,6 @@ OWN: dict[str, dict[str, str]] = {
"A busy model is waited for, then the next free one is used.": "Se espera a un modelo ocupado y luego se usa el siguiente libre.",
"A result, but not a run with nothing to report.": "Un resultado, pero no una ejecución sin nada que informar.",
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Una tarea sin modelo propio se ejecuta en el primero libre de estos, en este orden. Ninguno marcado: todos.",
"A team of up to %(n)s": "Un equipo de hasta %(n)s",
"A team works on it together, its own agent included.": "Un equipo trabaja en ella, incluido su propio agente.",
"Choose a model to choose its effort.": "Elija un modelo para elegir su esfuerzo.",
"Due %(when)s": "Prevista %(when)s",
"Each run sees the ones before it.": "Cada ejecución ve las anteriores.",
@@ -938,7 +1018,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "Editar %(title)s",
"Every result, even with nothing to report.": "Cada resultado, incluso sin nada que informar.",
"Models for scheduled tasks": "Modelos para tareas programadas",
"No team": "Sin equipo",
"Only in its history.": "Solo en su historial.",
"Open the chat": "Abrir el chat",
"Result": "Resultado",
@@ -959,6 +1038,30 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Ningún día marcado: todos los días.",
},
"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.",
"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",
"Automatic compaction is off.": "Le compactage automatique est désactivé.",
"Compacts before the next reply.": "Compacté avant la prochaine réponse.",
"Compacts by itself once the window is known.": "Compacté de lui-même dès que la fenêtre est connue.",
"Compact": "Compacter",
"Compacting…": "Compactage…",
"Summarise the earlier turns now and send the summary in their place.": "Résumer maintenant les tours précédents et envoyer le résumé à leur place.",
"This server cannot compact a chat from the app yet; the web interface can.": "Ce serveur ne peut pas encore compacter une conversation depuis l’application ; l’interface web le peut.",
"Context and background tasks": "Contexte et tâches en arrière-plan",
"Environment": "Environnement",
"Finished %(n)s": "Terminées : %(n)s",
"Background command": "Commande en arrière-plan",
"Subagent": "Sous-agent",
"Team member": "Membre de l’équipe",
"Subagents": "Sous-agents",
"None": "Aucun",
"Up to %(n)s at once": "Jusqu’à %(n)s à la fois",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Rien ne tourne en arrière-plan. Les sous-agents, les membres d’une équipe et les commandes en arrière-plan s’affichent ici pendant qu’ils travaillent.",
"Team: %(size)s of at most %(max)s subagents": "Équipe : %(size)s sur %(max)s sous-agents au plus",
"How many work on it at once, the run's own agent among them, on the same model.": "Combien y travaillent à la fois, l’agent de l’exécution compris, avec le même modèle.",
"Menu": "Menu",
"Minimise": "Réduire",
"Maximise": "Agrandir",
@@ -1126,8 +1229,6 @@ OWN: dict[str, dict[str, str]] = {
"A busy model is waited for, then the next free one is used.": "Un modèle occupé est attendu, puis le suivant libre est utilisé.",
"A result, but not a run with nothing to report.": "Un résultat, mais pas une exécution sans rien à signaler.",
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Une tâche sans modèle à elle tourne sur le premier libre de ceux-ci, dans cet ordre. Aucun coché : tous.",
"A team of up to %(n)s": "Une équipe jusqu'à %(n)s",
"A team works on it together, its own agent included.": "Une équipe y travaille ensemble, son propre agent compris.",
"Choose a model to choose its effort.": "Choisissez un modèle pour choisir son effort.",
"Due %(when)s": "Prévue %(when)s",
"Each run sees the ones before it.": "Chaque exécution voit les précédentes.",
@@ -1136,7 +1237,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "Modifier %(title)s",
"Every result, even with nothing to report.": "Chaque résultat, même sans rien à signaler.",
"Models for scheduled tasks": "Modèles pour les tâches planifiées",
"No team": "Pas d'équipe",
"Only in its history.": "Seulement dans son historique.",
"Open the chat": "Ouvrir la conversation",
"Result": "Résultat",
@@ -1157,6 +1257,30 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Aucun jour coché : tous les jours.",
},
"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.",
"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",
"Automatic compaction is off.": "Az automatikus tömörítés ki van kapcsolva.",
"Compacts before the next reply.": "A következő válasz előtt tömörül.",
"Compacts by itself once the window is known.": "Magától tömörül, amint ismert az ablak.",
"Compact": "Tömörítés",
"Compacting…": "Tömörítés…",
"Summarise the earlier turns now and send the summary in their place.": "A korábbi köröket most összefoglalja, és helyettük az összefoglalót küldi.",
"This server cannot compact a chat from the app yet; the web interface can.": "Ez a szerver még nem tud beszélgetést tömöríteni az alkalmazásból; a webes felület tud.",
"Context and background tasks": "Kontextus és háttérfeladatok",
"Environment": "Környezet",
"Finished %(n)s": "Kész: %(n)s",
"Background command": "Háttérparancs",
"Subagent": "Alügynök",
"Team member": "Csapattag",
"Subagents": "Alügynökök",
"None": "Nincs",
"Up to %(n)s at once": "Legfeljebb %(n)s egyszerre",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "A háttérben semmi sem fut. Az alügynökök, egy csapat tagjai és a háttérparancsok itt jelennek meg, amíg dolgoznak.",
"Team: %(size)s of at most %(max)s subagents": "Csapat: %(size)s a legfeljebb %(max)s alügynökből",
"How many work on it at once, the run's own agent among them, on the same model.": "Hányan dolgoznak rajta egyszerre, a futás saját ügynökét is beleértve, ugyanazon a modellen.",
"Menu": "Menü",
"Minimise": "Kis méret",
"Maximise": "Teljes méret",
@@ -1324,8 +1448,6 @@ OWN: dict[str, dict[str, str]] = {
"A busy model is waited for, then the next free one is used.": "A foglalt modellre vár, majd a következő szabadot használja.",
"A result, but not a run with nothing to report.": "Eredmény, de nem olyan futás, amelynek nincs mit jelentenie.",
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "A saját modell nélküli feladat ezek közül az első szabadon fut, ebben a sorrendben. Ha egy sincs bejelölve: mindegyiken.",
"A team of up to %(n)s": "Legfeljebb %(n)s fős csapat",
"A team works on it together, its own agent included.": "Egy csapat dolgozik rajta együtt, a saját ügynökét is beleértve.",
"Choose a model to choose its effort.": "Válasszon modellt az erőfeszítés megadásához.",
"Due %(when)s": "Esedékes: %(when)s",
"Each run sees the ones before it.": "Minden futás látja az előzőeket.",
@@ -1334,7 +1456,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "%(title)s szerkesztése",
"Every result, even with nothing to report.": "Minden eredmény, akkor is, ha nincs mit jelenteni.",
"Models for scheduled tasks": "Modellek az ütemezett feladatokhoz",
"No team": "Nincs csapat",
"Only in its history.": "Csak az előzményeiben.",
"Open the chat": "A csevegés megnyitása",
"Result": "Eredmény",
@@ -1355,6 +1476,30 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Ha egy nap sincs bejelölve: minden nap.",
},
"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.",
"Tasks: %(n)s": "Attività: %(n)s",
"%(n)s running": "%(n)s in corso",
"%(tokens)s until auto-compact": "%(tokens)s alla compattazione automatica",
"Automatic compaction is off.": "La compattazione automatica è disattivata.",
"Compacts before the next reply.": "Si compatta prima della prossima risposta.",
"Compacts by itself once the window is known.": "Si compatta da sola appena la finestra è nota.",
"Compact": "Compatta",
"Compacting…": "Compattazione…",
"Summarise the earlier turns now and send the summary in their place.": "Riassumi ora i turni precedenti e invia il riassunto al loro posto.",
"This server cannot compact a chat from the app yet; the web interface can.": "Questo server non può ancora compattare una chat dall’app; l’interfaccia web sì.",
"Context and background tasks": "Contesto e attività in background",
"Environment": "Ambiente",
"Finished %(n)s": "Terminate: %(n)s",
"Background command": "Comando in background",
"Subagent": "Sottoagente",
"Team member": "Membro della squadra",
"Subagents": "Sottoagenti",
"None": "Nessuno",
"Up to %(n)s at once": "Fino a %(n)s alla volta",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Nulla è in esecuzione in background. Sottoagenti, membri di una squadra e comandi in background compaiono qui mentre lavorano.",
"Team: %(size)s of at most %(max)s subagents": "Squadra: %(size)s su un massimo di %(max)s sottoagenti",
"How many work on it at once, the run's own agent among them, on the same model.": "Quanti ci lavorano alla volta, compreso l’agente dell’esecuzione, sullo stesso modello.",
"Menu": "Menu",
"Minimise": "Riduci a icona",
"Maximise": "Ingrandisci",
@@ -1522,8 +1667,6 @@ OWN: dict[str, dict[str, str]] = {
"A busy model is waited for, then the next free one is used.": "Si attende un modello occupato, poi si usa il successivo libero.",
"A result, but not a run with nothing to report.": "Un risultato, ma non un’esecuzione senza nulla da segnalare.",
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Un’attività senza un modello proprio gira sul primo libero di questi, in quest’ordine. Nessuno spuntato: tutti.",
"A team of up to %(n)s": "Un team fino a %(n)s",
"A team works on it together, its own agent included.": "Un team ci lavora insieme, incluso il suo agente.",
"Choose a model to choose its effort.": "Scegli un modello per sceglierne l’impegno.",
"Due %(when)s": "Prevista %(when)s",
"Each run sees the ones before it.": "Ogni esecuzione vede quelle precedenti.",
@@ -1532,7 +1675,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "Modifica %(title)s",
"Every result, even with nothing to report.": "Ogni risultato, anche senza nulla da segnalare.",
"Models for scheduled tasks": "Modelli per le attività pianificate",
"No team": "Nessun team",
"Only in its history.": "Solo nella sua cronologia.",
"Open the chat": "Apri la chat",
"Result": "Risultato",
@@ -1553,6 +1695,30 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Nessun giorno spuntato: ogni giorno.",
},
"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.",
"Tasks: %(n)s": "Zadania: %(n)s",
"%(n)s running": "Działa: %(n)s",
"%(tokens)s until auto-compact": "%(tokens)s do automatycznego streszczenia",
"Automatic compaction is off.": "Automatyczne streszczanie jest wyłączone.",
"Compacts before the next reply.": "Zostanie streszczona przed następną odpowiedzią.",
"Compacts by itself once the window is known.": "Zostanie streszczona sama, gdy okno będzie znane.",
"Compact": "Streść",
"Compacting…": "Streszczanie…",
"Summarise the earlier turns now and send the summary in their place.": "Streścić teraz wcześniejsze tury i wysyłać streszczenie zamiast nich.",
"This server cannot compact a chat from the app yet; the web interface can.": "Ten serwer nie umie jeszcze streścić czatu z aplikacji; interfejs WWW umie.",
"Context and background tasks": "Kontekst i zadania w tle",
"Environment": "Środowisko",
"Finished %(n)s": "Zakończone: %(n)s",
"Background command": "Polecenie w tle",
"Subagent": "Podagent",
"Team member": "Członek zespołu",
"Subagents": "Podagenci",
"None": "Brak",
"Up to %(n)s at once": "Do %(n)s naraz",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "W tle nic nie działa. Podagenci, członkowie zespołu i polecenia w tle pojawiają się tu, gdy pracują.",
"Team: %(size)s of at most %(max)s subagents": "Zespół: %(size)s z najwyżej %(max)s podagentów",
"How many work on it at once, the run's own agent among them, on the same model.": "Ilu pracuje nad nim naraz, łącznie z własnym agentem uruchomienia, na tym samym modelu.",
"Menu": "Menu",
"Minimise": "Minimalizuj",
"Maximise": "Maksymalizuj",
@@ -1720,8 +1886,6 @@ OWN: dict[str, dict[str, str]] = {
"A busy model is waited for, then the next free one is used.": "Na zajęty model się czeka, potem używany jest następny wolny.",
"A result, but not a run with nothing to report.": "Wynik, ale nie uruchomienie bez niczego do zgłoszenia.",
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Zadanie bez własnego modelu działa na pierwszym wolnym z tych, w tej kolejności. Żaden niezaznaczony: wszystkie.",
"A team of up to %(n)s": "Zespół do %(n)s",
"A team works on it together, its own agent included.": "Pracuje nad nim zespół, wraz z jego własnym agentem.",
"Choose a model to choose its effort.": "Wybierz model, aby wybrać jego wysiłek.",
"Due %(when)s": "Zaplanowane na %(when)s",
"Each run sees the ones before it.": "Każde uruchomienie widzi poprzednie.",
@@ -1730,7 +1894,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "Edytuj %(title)s",
"Every result, even with nothing to report.": "Każdy wynik, nawet bez niczego do zgłoszenia.",
"Models for scheduled tasks": "Modele dla zaplanowanych zadań",
"No team": "Bez zespołu",
"Only in its history.": "Tylko w jego historii.",
"Open the chat": "Otwórz czat",
"Result": "Wynik",
@@ -1751,6 +1914,30 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Żaden dzień niezaznaczony: codziennie.",
},
"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.",
"Tasks: %(n)s": "Úlohy: %(n)s",
"%(n)s running": "Beží: %(n)s",
"%(tokens)s until auto-compact": "%(tokens)s do automatického zhrnutia",
"Automatic compaction is off.": "Automatické zhrnutie je vypnuté.",
"Compacts before the next reply.": "Zhrnie sa pred ďalšou odpoveďou.",
"Compacts by itself once the window is known.": "Zhrnie sa samo, keď bude okno známe.",
"Compact": "Zhrnúť",
"Compacting…": "Zhŕňa sa…",
"Summarise the earlier turns now and send the summary in their place.": "Zhrnúť skoršie kolá hneď a posielať namiesto nich zhrnutie.",
"This server cannot compact a chat from the app yet; the web interface can.": "Tento server zatiaľ nevie zhrnúť konverzáciu z aplikácie; webové rozhranie to vie.",
"Context and background tasks": "Kontext a úlohy na pozadí",
"Environment": "Prostredie",
"Finished %(n)s": "Dokončené: %(n)s",
"Background command": "Príkaz na pozadí",
"Subagent": "Podagent",
"Team member": "Člen tímu",
"Subagents": "Podagenti",
"None": "Žiadni",
"Up to %(n)s at once": "Najviac %(n)s naraz",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Na pozadí nič nebeží. Podagenti, členovia tímu a príkazy na pozadí sa tu zobrazia, kým pracujú.",
"Team: %(size)s of at most %(max)s subagents": "Tím: %(size)s z najviac %(max)s podagentov",
"How many work on it at once, the run's own agent among them, on the same model.": "Koľko na nej pracuje naraz, vrátane vlastného agenta behu, na rovnakom modeli.",
"Menu": "Ponuka",
"Minimise": "Minimalizovať",
"Maximise": "Maximalizovať",
@@ -1918,8 +2105,6 @@ OWN: dict[str, dict[str, str]] = {
"A busy model is waited for, then the next free one is used.": "Na zaneprázdnený model sa čaká, potom sa použije ďalší voľný.",
"A result, but not a run with nothing to report.": "Výsledok, ale nie spustenie, ktoré nemá čo hlásiť.",
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Úloha bez vlastného modelu beží na prvom voľnom z týchto, v tomto poradí. Žiadny nezaškrtnutý: všetky.",
"A team of up to %(n)s": "Tím: najviac %(n)s",
"A team works on it together, its own agent included.": "Pracuje na nej tím spoločne, vrátane jej vlastného agenta.",
"Choose a model to choose its effort.": "Vyberte model, aby ste mohli zvoliť jeho náročnosť.",
"Due %(when)s": "Naplánované na %(when)s",
"Each run sees the ones before it.": "Každé spustenie vidí tie predchádzajúce.",
@@ -1928,7 +2113,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "Upraviť %(title)s",
"Every result, even with nothing to report.": "Každý výsledok, aj keď nie je čo hlásiť.",
"Models for scheduled tasks": "Modely pre naplánované úlohy",
"No team": "Bez tímu",
"Only in its history.": "Len v jej histórii.",
"Open the chat": "Otvoriť konverzáciu",
"Result": "Výsledok",
+59 -18
View File
@@ -122,6 +122,7 @@ textarea { padding: var(--sp-2) var(--control-px-sm); resize: none; line-height:
label, .label { font: var(--text-sm) / var(--leading-tight) var(--font-body); color: var(--ink-muted); }
.hint { font-size: var(--text-sm); color: var(--ink-muted); min-height: calc(var(--text-sm) * var(--leading-normal)); }
.hint.is-warn { color: var(--warning); }
.status { font: var(--text-sm) / var(--leading-normal) var(--font-body); }
.status.is-ok { color: var(--leaf); }
.check { display: grid; grid-template-columns: var(--check-size) minmax(0, 1fr); gap: var(--sp-2); align-items: center; color: var(--ink); }
@@ -708,17 +709,19 @@ 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 team's size: two number fields of one width with a dash between them, and in the header
/* 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, .agents__heads {
display: inline-grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
grid-template-columns: minmax(0, 1fr) var(--sp-3) minmax(0, 1fr);
gap: var(--sp-2);
align-items: center;
width: 100%;
height: var(--control-h);
}
.agents__dash { color: var(--ink-muted); }
.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);
@@ -920,6 +923,14 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
.ctx__name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--ink-muted); }
.ctx__num { font: var(--text-xs) var(--font-mono); text-align: right; color: var(--ink-muted); }
.ctx__note { margin: 0; font-size: var(--text-xs); color: var(--ink-faint); }
/* "41k until auto-compact" and the Compact button: the words give, the button keeps its size. */
.ctx__compact { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-1) var(--sp-2); align-items: center; padding-top: var(--sp-2); border-top: var(--border-w) solid var(--border); }
.ctx__until { color: var(--ink-muted); }
.ctx__error { grid-column: 1 / -1; margin: 0; font-size: var(--text-xs); color: var(--warning); }
/* Environment: an agent chat's folder and where it is. */
.env { display: grid; grid-template-columns: var(--sp-4) minmax(0, auto) auto; justify-content: start; gap: var(--sp-2); align-items: center; margin: 0; }
.env__name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--ink); }
.env__where { font: var(--text-xs) var(--font-mono); color: var(--ink-muted); }
.ctx--messages { --ctx: var(--accent); }
.ctx--system { --ctx: var(--ink-muted); }
.ctx--memory { --ctx: var(--warning); }
@@ -943,15 +954,41 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
track that gives, cut), elapsed time in a column of its own width, the stop button at its size
(an empty box where there is none) -- so rows line up whatever each says. */
.tasks { display: grid; gap: var(--sp-2); min-width: 0; }
.tasks__summary, .tasks__team { margin: 0; color: var(--ink-muted); }
.tasks__team { margin: 0; color: var(--ink-muted); }
.tasks__goal { margin: 0; color: var(--ink); }
.tasks__list { display: grid; gap: var(--sp-1); }
.tasks__more > summary { cursor: pointer; padding: var(--sp-1) 0; color: var(--ink-muted); }
.tasks__more[open] > summary { margin-bottom: var(--sp-1); }
.tasks__group { display: grid; grid-template-columns: auto auto; justify-content: start; gap: var(--sp-2); align-items: baseline; margin: var(--sp-1) 0 0; color: var(--ink-muted); }
.tasks__list { display: grid; gap: var(--sp-2); }
/* A fold ("Show 3 more", "Finished 12", a task's own "2 tasks"): a chevron that turns, then the
words; nested ones step in by the depth of what is under them. */
.tasks__more { margin-left: calc(var(--depth, 0) * var(--sp-4)); min-width: 0; }
.tasks__more > summary {
display: grid;
grid-template-columns: 12px minmax(0, 1fr);
gap: var(--sp-2);
align-items: center;
min-height: var(--row-h);
padding: 0 var(--sp-3);
color: var(--ink-muted);
cursor: pointer;
list-style: none;
}
.tasks__more > summary::-webkit-details-marker { display: none; }
.tasks__more > summary:hover { color: var(--ink); }
.tasks__more[open] > summary { margin-bottom: var(--sp-2); }
.tasks__nested { margin-top: calc(-1 * var(--sp-1)); }
.tasks__finished { padding-top: var(--sp-1); border-top: var(--border-w) solid var(--border); }
.tasks__chev, .agent__chev { display: grid; place-items: center; width: 12px; height: 12px; color: var(--ink-faint); transition: transform var(--dur-1) var(--ease-out); }
.tasks__chev .icon, .agent__chev .icon { width: 12px; height: 12px; }
.tasks__more[open] > summary .tasks__chev, .agent[open] > .agent__row .agent__chev { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .tasks__chev, .agent__chev { transition: none; } }
/* A task: one card, one row of known tracks -- the chevron, what it does (the one track that
gives, cut), the elapsed time in a column of its own width, the stop square at its size (an
empty box where it cannot be stopped) -- so the rows line up whatever each says. */
.agent { margin-left: calc(var(--depth, 0) * var(--sp-4)); background: var(--surface); border: var(--border-w) solid var(--border); border-radius: var(--radius-lg); min-width: 0; }
.agent__row {
display: grid;
grid-template-columns: var(--dot-size) minmax(0, 1fr) var(--time-col) var(--control-h-sm);
grid-template-columns: 12px minmax(0, 1fr) var(--time-col) var(--control-h-sm);
gap: var(--sp-2);
align-items: center;
min-height: var(--control-h);
@@ -960,17 +997,21 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
list-style: none;
}
.agent__row::-webkit-details-marker { display: none; }
.agent__dot { width: var(--dot-size); height: var(--dot-size); border-radius: var(--radius-full); background: var(--ink-faint); }
.agent.is-working .agent__dot { background: var(--accent); }
.agent.is-queued .agent__dot, .agent.is-waiting .agent__dot { background: var(--warning); }
.agent.is-done .agent__dot { background: var(--leaf); }
.agent.is-error .agent__dot { background: var(--danger); }
.agent__row:hover { background: var(--surface-hover); border-radius: var(--radius-lg); }
.agent__what { display: grid; min-width: 0; }
.agent__name, .agent__role { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.agent__name { color: var(--ink); }
.agent__role { font-size: var(--text-xs); color: var(--ink-muted); }
.agent__name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--ink); }
.agent.is-done .agent__name, .agent.is-stopped .agent__name { color: var(--ink-muted); }
.agent.is-error .agent__name { color: var(--danger); }
.agent__role { margin: 0; color: var(--ink-muted); overflow-wrap: anywhere; }
.agent__time { font: var(--text-xs) var(--font-mono); text-align: right; color: var(--ink-muted); }
.agent__stop { width: var(--control-h-sm); height: var(--control-h-sm); }
.agent.is-queued .agent__time, .agent.is-waiting .agent__time { color: var(--warning); }
/* The stop square: a small filled square in a button the size of the others. */
.agent__stop { display: grid; place-items: center; width: var(--control-h-sm); height: var(--control-h-sm); padding: 0; background: var(--surface-active); border: 0; border-radius: var(--radius-md); cursor: pointer; }
.agent__stop:hover { background: var(--surface-hover); }
.agent__stop.is-none { background: none; cursor: default; }
.agent__square { width: var(--sp-3); height: var(--sp-3); background: var(--ink); border-radius: var(--radius-sm); }
.agent__stop:hover .agent__square { background: var(--danger); }
.agent__id { font: var(--text-xs) var(--font-mono); color: var(--ink-faint); overflow-wrap: anywhere; }
.agent__body { display: grid; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3) var(--sp-3); border-top: var(--border-w) solid var(--border); min-width: 0; }
.agent__body > .btn { justify-self: start; }
.agent__facts { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); margin: 0; font-size: var(--text-xs); color: var(--ink-muted); }
+192 -72
View File
File diff suppressed because it is too large. Load diff
+223 -76
View File
@@ -1,14 +1,18 @@
// The chat's activity panel, beside it: what is in its context, by category, against the model's
// window; the files the chat and its agents wrote; every background agent -- a team's members as
// a tree, helpers and commands -- with its state, how long it has run, its model, its latest step
// and what it is writing, its whole conversation read-only and a button to stop it; the team's
// room; and the plan. The strip above the message box sums the agents up and opens it. The same
// facts as the server's own panel (`services/activity.py`), from /api/v2:
// The chat's activity panel, beside it, opened from the header's button at any time: where an
// agent chat works; what is in its context, by category, against the model's window, how far it
// is to the automatic compaction and a button to compact now; the files the chat and its
// subagents wrote; every background task -- subagents, a team's members as a tree under the one
// that leads them, background commands -- with how long it has run and a stop square, unfolding
// to its latest step, what it is writing and its whole conversation, read-only; the finished ones
// folded; the team's room; and the plan. The line above the message box sums up what runs, while
// anything does, and opens it. The same facts as the server's own panel (`services/activity.py`),
// from /api/v2:
//
// GET /chats/{chat}/activity?since=&context= everything, with a `version`
// GET /chats/{chat}/activity/events `activity` {version, summary} on each change
// GET /chats/{chat}/agents/{kind}/{id} one agent and its conversation
// GET /chats/{chat}/agents/{kind}/{id} one task and its conversation
// POST /chats/{chat}/agents/{kind}/{id}/stop
// POST /chats/{chat}/compact summarise the earlier turns now
//
// The events stream is followed for as long as a chat is open: it keeps the strip current, and
// while the panel is open a new `version` reads the panel again. The context is the costly part
@@ -31,6 +35,8 @@ const SHOWN = 6
const CONTEXT_EVERY = 15000
let layout = () => {} // (on) -> the shell makes room for the panel
let onThisComputer = () => false // (chat) -> whether an agent chat works on this computer
let onCompacted = () => {} // (chat id) -> the conversation is read again
let chat = null // the open chat: { id, kind, … }
let stream = null // the events stream's handle
let retry = 0 // the reconnect timer
@@ -69,12 +75,14 @@ export function compact(value) {
return `${trim(n / 1000000, 2)}M`
}
/** "3 agents working · 1 queued · 2 finished", or "" for none (the server's `summary_text`). */
/**
* "3 running · 1 queued · 2 finished", or "" for none: every background task -- a subagent, a
* team's member, a command -- counts the same, as the panel lists them together.
*/
export function summaryText(counts) {
if (!counts) return ""
const parts = []
if (counts.working === 1) parts.push(t("1 agent working"))
else if (counts.working) parts.push(t("%(n)s agents working", { n: counts.working }))
if (counts.working) parts.push(t("%(n)s running", { n: counts.working }))
if (counts.queued) parts.push(t("%(n)s queued", { n: counts.queued }))
if (counts.waiting) parts.push(t("%(n)s waiting", { n: counts.waiting }))
if (counts.finished) parts.push(t("%(n)s finished", { n: counts.finished }))
@@ -141,9 +149,10 @@ export function watchActivity(next) {
cursor = 0
readAt = contextAt = 0
view = null
compactError = ""
openRows.clear()
renderStrip()
$("activity-open")?.setAttribute("aria-expanded", String(panelOpen()))
markOpen()
if (!chat) {
if (panelOpen()) showPanel(false)
return
@@ -269,36 +278,58 @@ async function read(withContext) {
}
}
// --- The strip above the message box ---------------------------------------------------------------
// --- The line above the message box, and the header's button ---------------------------------------
/** Both ways into the panel say whether it is open. */
function markOpen() {
for (const id of ["activity-open", "activity-strip-btn"]) $(id)?.setAttribute("aria-expanded", String(panelOpen()))
}
function renderStrip() {
const strip = $("activity-strip")
// A chip for every open chat: what runs, while something does, as the server's strip says it (a
// chat whose helpers all finished long ago does not carry "12 finished" for ever: the panel
// still lists them); else only the way into the panel.
strip.hidden = !chat
if (!chat) return clear(strip)
const active = summary?.active ? summaryText(summary) : ""
const text = active || t("Activity")
// Only while something runs, as the server's own line: the header's button is the way into the
// panel at any other time (and a chat whose tasks all finished long ago does not carry
// "12 finished" for ever: the panel still lists them).
const active = chat && summary?.active ? summaryText(summary) : ""
strip.hidden = !active
if (!active) return clear(strip)
strip.replaceChildren(
h(
"button",
{
type: "button",
id: "activity-open",
class: `strip__btn${active ? " is-active" : ""}`,
"aria-label": `${text} — ${panelOpen() ? t("Close the activity panel") : t("Open the activity panel")}`,
id: "activity-strip-btn",
class: "strip__btn is-active",
"aria-label": `${active} — ${panelOpen() ? t("Close the activity panel") : t("Open the activity panel")}`,
"aria-controls": "activity",
"aria-expanded": String(panelOpen()),
onclick: togglePanel,
},
h("span", { class: "dot", "aria-hidden": "true" }),
h("span", { class: "strip__text", text }),
h("span", { class: "strip__text", text: active }),
icon(ICONS.chevron),
),
)
}
/** The header's button: the panel, open or closed, for every open chat. */
export function activityButton() {
return h(
"button",
{
type: "button",
class: "tool",
id: "activity-open",
"aria-label": t("Context and background tasks"),
title: t("Context and background tasks"),
"aria-controls": "activity",
"aria-expanded": String(panelOpen()),
onclick: togglePanel,
},
icon(ICONS.tasks),
)
}
// --- The panel ------------------------------------------------------------------------------------------
/** Open or close the panel (the header's Activity button, the strip, Escape). */
@@ -306,14 +337,16 @@ export function showPanel(on) {
if (on && !chat) return
$("activity").hidden = !on
layout(on)
$("activity-open")?.setAttribute("aria-expanded", String(on))
markOpen()
clearInterval(ticker)
ticker = 0
if (!on) {
view = null
renderStrip()
return
}
ticker = setInterval(tick, 1000)
renderStrip()
render()
read(true)
$("activity-close").focus()
@@ -359,15 +392,43 @@ function render() {
}
// Kept where the reader was: the panel is drawn again on every change.
const scroll = body.scrollTop
body.replaceChildren(
const entries = data.agents || []
const parts = [
chat?.kind === "agent" && chat.device ? section("environment", t("Environment"), 0, environmentPart(chat)) : null,
section("context", t("Context"), 0, contextPart(data.context)),
section("outputs", t("Outputs"), (data.outputs || []).length, outputsPart(data.outputs || [])),
section("tasks", t("Background tasks"), 0, tasksPart()),
section("tasks", t("Background tasks"), 0, tasksPart(entries)),
data.team ? section("room", t("Room"), room.length, roomPart()) : null,
data.plan ? section("plan", t("Plan"), 0, h("div", { class: "activity__plan" }, planList(data.plan))) : null,
)
]
body.replaceChildren(...parts.filter(Boolean))
body.scrollTop = scroll
}
/** How many tasks, nested ones included: those still at work, or those finished. */
function count(list, active) {
let n = 0
for (const a of list) {
if (ACTIVE.includes(a.state) === active) n += 1
n += count(a.children || [], active)
}
return n
}
/** Where an agent chat works: its folder, on this computer or another device. */
function environmentPart(c) {
const dir = c.device?.directory || ""
const name = dir.split(/[\\/]/).filter(Boolean).pop() || dir
const here = onThisComputer(c)
return h(
"p",
{ class: "env", title: dir },
icon(ICONS.folder),
h("bdi", { class: "env__name", text: name }),
h("span", { class: "env__where", text: here ? t("This computer") : t("Another device") }),
)
}
function contextPart(c) {
if (!c) return h("p", { class: "activity__empty", text: t("Not measured.") })
const known = c.percent !== null && c.percent !== undefined
@@ -410,7 +471,67 @@ function contextPart(c) {
{ class: "ctx__note" },
`${c.measured ? t("Counted by the model at the last reply; the categories are estimated.") : t("Estimated: no reply has been counted yet.")} ${t("%(n)s tools offered.", { n: c.tools_count ?? 0 })}`,
)
return h("div", { class: "ctx" }, used, bar, rows, note)
return h("div", { class: "ctx" }, used, bar, rows, compactRow(c), note)
}
/**
* How far it is to the automatic compaction, and the button that compacts now. The server says it
* (2.0.0-beta.7: `auto_compact`, `until_compact` in tokens, `compactable` whether Compact can run
* now); an older one gives only `threshold` -- the server compacts before a turn once the context
* would reach that % of the window, 0 is off -- and it is worked out from that.
*/
function compactRow(c) {
const limit = Number(c.limit) || 0
const threshold = Number(c.threshold)
const auto = "auto_compact" in c ? !!c.auto_compact : threshold > 0
let until = c.until_compact
if (until === undefined && auto && limit && threshold < 100) until = Math.floor((limit * threshold) / 100) - (Number(c.used) || 0)
let words
if (!auto) words = t("Automatic compaction is off.")
else if (until === null || until === undefined) words = t("Compacts by itself once the window is known.")
else words = until > 0 ? t("%(tokens)s until auto-compact", { tokens: compact(until) }) : t("Compacts before the next reply.")
const busy = compacting === chat?.id
// `compactable` false: nothing to summarise yet, a reply being written, compaction turned off --
// the button stays, greyed; pressing an older server's says why in its own words.
const can = !("compactable" in c) || !!c.compactable
return h(
"div",
{ class: "ctx__compact" },
h("span", { class: "ctx__until", text: words }),
h("button", {
type: "button",
class: "btn btn--sm",
text: busy ? t("Compacting…") : t("Compact"),
disabled: busy || !can,
title: t("Summarise the earlier turns now and send the summary in their place."),
onclick: compactNow,
}),
compactError ? h("p", { class: "ctx__error", role: "alert", text: compactError }) : null,
)
}
let compacting = null // the chat being compacted
let compactError = ""
async function compactNow() {
if (!chat || compacting) return
const id = chat.id
compacting = id
compactError = ""
render()
try {
await post(`/chats/${enc(id)}/compact`)
compacting = null
if (chat?.id !== id) return
onCompacted(id)
read(true)
} catch (err) {
compacting = null
if (chat?.id !== id) return
// A server without the route in its /api/v2 says 404 or 405: said as what it means.
compactError = err?.status === 404 || err?.status === 405 ? t("This server cannot compact a chat from the app yet; the web interface can.") : why(err)
render()
}
}
/** A category's key as a class name: the server's words, kept to letters and underscores. */
@@ -439,64 +560,71 @@ function outputsPart(outputs) {
)
}
function tasksPart() {
const entries = data.agents || []
function tasksPart(entries) {
const team = data.team
const box = h("div", { class: "tasks", "aria-live": "polite" })
if (!entries.length && !team) {
box.append(h("p", { class: "activity__empty", text: t("Nothing runs in the background. Helpers, commands and a team's members show here while they work.") }))
box.append(h("p", { class: "activity__empty", text: t("Nothing runs in the background. Subagents, a team's members and background commands show here while they work.") }))
return box
}
box.append(h("p", { class: "tasks__summary", text: summaryText(data.summary) || t("Nothing running.") }))
if (team) {
box.append(h("p", { class: "tasks__team", text: t("Team: %(size)s of %(max)s agents", { size: team.size, max: team.max }) }))
box.append(h("p", { class: "tasks__team", text: t("Team: %(size)s of at most %(max)s subagents", { size: team.size, max: team.max }) }))
if (team.goal) box.append(h("p", { class: "tasks__goal", text: team.goal }))
}
const live = entries.filter((e) => ACTIVE.includes(e.state))
const ended = entries.filter((e) => !ACTIVE.includes(e.state))
// At the top level, what still runs -- with its own tasks under it, finished or not -- and then
// what finished with nothing under it still at work, folded.
const live = entries.filter((e) => ACTIVE.includes(e.state) || count(e.children || [], true))
const ended = entries.filter((e) => !live.includes(e))
box.append(h("p", { class: "tasks__group" }, h("span", { text: t("Running") }), h("span", { class: "activity__count", text: String(count(entries, true)) })))
if (live.length) {
box.append(h("div", { class: "tasks__list" }, live.slice(0, SHOWN).map(agentRows)))
if (live.length > SHOWN) box.append(fold("more", t("Show %(n)s more", { n: live.length - SHOWN }), live.slice(SHOWN)))
}
if (ended.length) box.append(fold("finished", t("%(n)s finished", { n: ended.length }), ended))
if (team) {
box.append(h("h3", { class: "activity__subheading", text: t("Room") }))
box.append(
room.length
? h(
"ol",
{ class: "room" },
room.map((m) =>
h(
"li",
{ class: `room__entry${m.report ? " is-report" : ""}` },
h(
"div",
{ class: "room__meta" },
h("span", { class: "room__n", text: `#${m.n}` }),
h("span", { class: "room__route", text: `${m.from} → ${(m.to || []).join(", ")}` }),
m.report ? h("span", { class: "chip room__badge", text: t("report") }) : null,
),
h("div", { class: "room__text md" }, markdown(String(m.text || ""))),
),
),
)
: h("p", { class: "activity__empty", text: t("Nothing has been said in the room yet.") }),
)
}
} else box.append(h("p", { class: "activity__empty", text: t("Nothing running.") }))
if (ended.length) box.append(fold("finished", t("Finished %(n)s", { n: count(ended, false) }), ended, "tasks__finished"))
return box
}
/** A folded list of agents ("Show 3 more", "12 finished"), remembered open across redraws. */
function fold(name, label, list) {
const el = h("details", { class: "tasks__more", open: openRows.has(`fold:${name}`) }, h("summary", { text: label }), h("div", { class: "tasks__list" }, list.map(agentRows)))
function roomPart() {
if (!room.length) return h("p", { class: "activity__empty", text: t("Nothing has been said in the room yet.") })
return h(
"ol",
{ class: "room" },
room.map((m) =>
h(
"li",
{ class: `room__entry${m.report ? " is-report" : ""}` },
h(
"div",
{ class: "room__meta" },
h("span", { class: "room__n", text: `#${m.n}` }),
h("span", { class: "room__route", text: `${m.from} → ${(m.to || []).join(", ")}` }),
m.report ? h("span", { class: "chip room__badge", text: t("report") }) : null,
),
h("div", { class: "room__text md" }, markdown(String(m.text || ""))),
),
),
)
}
/** A folded list of tasks ("Show 3 more", "Finished 12", "Tasks: 2"), remembered across redraws. */
function fold(name, label, list, cls = "") {
const el = h(
"details",
{ class: `tasks__more ${cls}`, open: openRows.has(`fold:${name}`) },
h("summary", null, h("span", { class: "tasks__chev", "aria-hidden": "true" }, icon(ICONS.chevron)), h("span", { text: label })),
h("div", { class: "tasks__list" }, list.map(agentRows)),
)
el.addEventListener("toggle", () => (el.open ? openRows.add(`fold:${name}`) : openRows.delete(`fold:${name}`)))
return el
}
/** An agent and, under it, the agents it leads or started. */
/** A task and, folded under it, the tasks it leads or started ("Tasks: 3"). */
function agentRows(a) {
return [agentRow(a), ...(a.children || []).map(agentRows)]
const kids = a.children || []
if (!kids.length) return agentRow(a)
const nested = fold(`under:${a.kind}:${a.id}`, t("Tasks: %(n)s", { n: kids.length }), kids, "tasks__nested")
nested.style.setProperty("--depth", String(Math.max(0, Math.min(6, (Number(a.depth) || 0) + 1))))
return [agentRow(a), nested]
}
function agentRow(a) {
@@ -512,7 +640,7 @@ function agentRow(a) {
"button",
{
type: "button",
class: "pane__btn agent__stop",
class: "agent__stop",
"aria-label": t("Stop %(name)s", { name: a.name }),
title: t("Stop %(name)s", { name: a.name }),
onclick: (e) => {
@@ -521,13 +649,15 @@ function agentRow(a) {
stopAgent(a)
},
},
icon(ICONS.stop),
h("span", { class: "agent__square", "aria-hidden": "true" }),
)
: h("span", { class: "agent__stop", "aria-hidden": "true" })
: h("span", { class: "agent__stop is-none", "aria-hidden": "true" })
const facts = h(
"p",
{ class: "agent__facts" },
h("span", { class: "agent__state", text: stateWord(a.state) }),
h("span", { text: kindWord(a) }),
a.kind !== "member" && a.role ? h("code", { class: "agent__id", text: a.name || a.id }) : null,
a.agent ? h("span", { text: a.agent }) : null,
a.model ? h("span", { class: "agent__model", text: a.model }) : null,
a.steps ? h("span", { text: t("%(steps)s steps, %(tools)s tools", { steps: a.steps, tools: a.tools }) }) : null,
@@ -547,8 +677,8 @@ function agentRow(a) {
h(
"summary",
{ class: "agent__row" },
h("span", { class: "agent__dot", "aria-hidden": "true" }),
h("span", { class: "agent__what" }, h("span", { class: "agent__name", text: a.name || a.id }), a.role ? h("span", { class: "agent__role", text: a.role }) : null),
h("span", { class: "agent__chev", "aria-hidden": "true" }, icon(ICONS.chevron)),
h("span", { class: "agent__what", title: label(a) }, h("bdi", { class: "agent__name", text: label(a) })),
time,
stopBtn,
),
@@ -556,12 +686,11 @@ function agentRow(a) {
"div",
{ class: "agent__body" },
facts,
a.kind === "member" && a.role ? h("p", { class: "agent__role", text: a.role }) : null,
a.brief ? h("p", { class: "agent__brief", text: a.brief }) : null,
step,
a.text ? h("p", { class: "agent__text", text: a.text }) : null,
a.transcript
? h("button", { type: "button", class: "btn btn--sm", text: t("Open its conversation"), onclick: () => openAgent(a) })
: null,
a.transcript ? h("button", { type: "button", class: "btn btn--sm", text: t("Open its conversation"), onclick: () => openAgent(a) }) : null,
),
)
el.style.setProperty("--depth", String(Math.max(0, Math.min(6, Number(a.depth) || 0))))
@@ -569,6 +698,22 @@ function agentRow(a) {
return el
}
/**
* A task's line: a team's member by its name (it is how the room and the others call it); a
* subagent or a command by what it was given to do, its id only where there is nothing else.
*/
function label(a) {
if (a.kind === "member") return a.name || a.id
return a.role || a.brief || a.name || a.id
}
/** What a task is: a team's member, a subagent, or a background command. */
function kindWord(a) {
if (a.kind === "member") return t("Team member")
if (a.type === "command") return t("Background command")
return t("Subagent")
}
async function stopAgent(a) {
if (!chat) return
if (a.kind === "member") {
@@ -667,8 +812,10 @@ async function readAgent() {
// --- Start ------------------------------------------------------------------------------------------
export function initActivity({ onLayout }) {
export function initActivity({ onLayout, here, compacted }) {
layout = onLayout || layout
onThisComputer = here || onThisComputer
onCompacted = compacted || onCompacted
$("activity-close").replaceChildren(icon(ICONS.close))
$("activity-close").addEventListener("click", () => showPanel(false))
$("activity-back").replaceChildren(icon(ICONS.back))
+36 -18
View File
@@ -13,9 +13,9 @@ import { markdown } from "./markdown.js"
import { openViewer, showKind } from "./viewer.js"
import { readLocal } from "./explorer.js"
import { menu, ask, showLine, popover, closePopover } from "./popup.js"
import { avatar, agentsControl, teamPossible, levelsOf, levelWord, picture } from "./models.js"
import { avatar, agentsControl, teamPossible, namedOnly, subagentsCap, SUBAGENTS_HINT, SUBAGENTS_OWN, levelsOf, levelWord, picture } from "./models.js"
import { readyIds, sending, sent, discard } from "./attach.js"
import { watchActivity, activityChanged, planList } from "./activity.js"
import { watchActivity, activityChanged, planList, activityButton } from "./activity.js"
import { agentBlock, agentReady, agentState, onAgentChange } from "./agent.js"
import { folderName } from "./list.js"
@@ -30,7 +30,7 @@ export function openChatId() {
// --- Reading -----------------------------------------------------------------------------------
function onThisComputer(chat) {
export function onThisComputer(chat) {
return !!chat?.device && ctx.thisDevices().includes(chat.device.token_id)
}
@@ -552,6 +552,7 @@ function renderHead(chat) {
)
}
}
tools.append(activityButton())
tools.append(h("button", { type: "button", class: "tool", "aria-label": t("More"), title: t("More"), onclick: (e) => chatMenu(e.currentTarget, open?.chat || chat) }, icon(ICONS.more)))
}
@@ -564,13 +565,23 @@ async function chatModels(chat) {
}
}
/** Whether this chat may have a team (`GET /chats/{chat}/team`): only a chat on the server. */
/**
* Whether this chat is offered subagents: `{ cap, own }` (the instance's most; whether only a model
* the person names could serve them), or null. The chat's own `subagents` where the server gives
* it (2.0.0-beta.7), else its team's word (`GET /chats/{chat}/team`) -- only a chat on the server.
*/
async function teamOffered(chat) {
if (chat.kind !== "chat") return false
const said = chat.subagents
if (said !== undefined && said !== null) {
const offered = typeof said === "object" ? said.offered : said
return offered ? { cap: subagentsCap(said), own: namedOnly(said) } : null
}
if (chat.kind !== "chat") return null
try {
return !!(await get(`/chats/${encodeURIComponent(chat.id)}/team`)).team?.offered
const team = (await get(`/chats/${encodeURIComponent(chat.id)}/team`)).team
return team?.offered ? { cap: subagentsCap(), own: false } : null
} catch {
return false
return null
}
}
@@ -640,26 +651,32 @@ function effortChip(model, value, onChange) {
})
}
/** The team's size: a chip saying it, opening the two fields. `value` { min, max } or empty. */
function agentsChip(value, onChange) {
/**
* The subagents a chat may run: a chip saying the fewest and the most, opening the two fields.
* `value` { min, max } or empty; `cap` the instance's most, where the server says it.
*/
function agentsChip(value, onChange, { cap, own } = {}) {
const said = value?.min || value?.max ? `${value.min ?? "…"}–${value.max ?? "…"}` : t("default")
return chip({
ico: ICONS.users,
label: `${t("Agents")} ${said}`,
title: t("Its own agent included. Above one, it can add members that do the work while it leads them."),
label: `${t("Subagents")} ${said}`,
title: own ? SUBAGENTS_OWN() : SUBAGENTS_HINT(),
cls: `cchip--subagents${own ? " is-warn" : ""}`,
onclick: (e) => {
const fields = agentsControl({ value, id: "agents-min" })
const fields = agentsControl({ value, id: "agents-min", cap })
popover(
e.currentTarget,
h(
"div",
{ class: "pop__body" },
h("label", { class: "pop__title", for: "agents-min", text: t("Agents") }),
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") })),
fields,
h("p", { class: "hint", text: t("Its own agent included. Above one, it can add members that do the work while it leads them.") }),
own ? h("p", { class: "hint is-warn", text: SUBAGENTS_OWN() }) : null,
h("p", { class: "hint", text: SUBAGENTS_HINT() }),
h("div", { class: "pop__actions" }, h("button", { type: "button", class: "btn btn--sm btn--primary", text: t("Save"), onclick: () => (closePopover(), onChange(fields.value)) })),
),
{ up: true, label: t("Agents") },
{ up: true, label: t("Subagents") },
)
},
})
@@ -705,7 +722,7 @@ async function renderChips() {
renderChips()
}, mayPick))
if (model) parts.push(effortChip(model, draft.effort, (l) => ((draft.effort = l), renderChips())))
if (draft.kind === "chat" && teamPossible(model)) parts.push(agentsChip(draft.agents, (a) => ((draft.agents = a), renderChips())))
if (draft.kind === "chat" && teamPossible(model)) parts.push(agentsChip(draft.agents, (a) => ((draft.agents = a), renderChips()), { cap: subagentsCap(), own: namedOnly(model?.subagents) }))
box.replaceChildren(...parts.filter(Boolean))
return
}
@@ -732,8 +749,9 @@ async function renderChips() {
if (chat.model) parts.push(modelChip(models, chat.model, (id) => save({ model: id }), mayPick))
parts.push(effortChip(current, chat.effort || levelsOf(current).start, (l) => save({ effort: l })))
box.replaceChildren(...parts.filter(Boolean))
if (!(await teamOffered(chat)) || drawing !== chipsDrawn || open?.chat?.id !== chat.id) return
box.append(agentsChip(open.chat.agents || chat.agents, (a) => save({ agents: a })))
const offer = await teamOffered(chat)
if (!offer || drawing !== chipsDrawn || open?.chat?.id !== chat.id) return
box.append(agentsChip(open.chat.agents || chat.agents, (a) => save({ agents: a }), offer))
}
/**
+1
View File
@@ -81,6 +81,7 @@ export const ICONS = {
attach: "M7.25 2.5h1.5v4.75h4.75v1.5H8.75v4.75h-1.5V8.75H2.5v-1.5h4.75z",
bolt: "M9.5 1L3 9.25h4.25L6.5 15 13 6.75H8.75z",
stop: "M4.5 4.5h7v7h-7z",
tasks: "M2 3.25h2v2H2zM5.5 3.5H14V5H5.5zM2 7h2v2H2zM5.5 7.25H14v1.5H5.5zM2 10.75h2v2H2zM5.5 11H14v1.5H5.5z",
back: "M10 3.5L5.5 8 10 12.5 11.1 11.4 7.7 8 11.1 4.6z",
terminal: "M1.5 2.5h13v11h-13zM3 4v8h10V4zM4.2 5.4l2.6 2.6-2.6 2.6-1-1L4.8 8 3.2 6.4zM7.5 10h4v1.5h-4z",
search: "M7 2a5 5 0 013.95 8.07l3.24 3.24-1.06 1.06-3.24-3.24A5 5 0 117 2zm0 1.5a3.5 3.5 0 100 7 3.5 3.5 0 000-7z",
+8 -2
View File
@@ -12,13 +12,14 @@ import { renderSettings, initSettings, scrollToSection } from "./settings.js"
import { initViewer, onViewer, closeViewer } from "./viewer.js"
import { initExplorer, showLocal, showFilesTab } from "./explorer.js"
import { initList, refreshList, markCurrent } from "./list.js"
import { initChat, openChat, newChat, closeChat, openChatId, chatMenu, statusChanged, draftKind } from "./chat.js"
import { initChat, openChat, newChat, closeChat, openChatId, chatMenu, statusChanged, draftKind, onThisComputer } from "./chat.js"
import { initAttach, allowAttach } from "./attach.js"
import { initPalette, openPalette } from "./palette.js"
import { initActivity, showPanel, togglePanel, panelOpen } from "./activity.js"
import { initAgent, link } from "./agent.js"
import { initScheduled, showScheduled, scheduledCommands } from "./scheduled.js"
import { closePopover } from "./popup.js"
import { noteSubagentsCap } from "./models.js"
let status = null
let view = "" // "setup" | "app"
@@ -118,7 +119,9 @@ function stopApp() {
async function loadModels() {
try {
models = (await get("/models")).models || []
const r = await get("/models")
models = r.models || []
noteSubagentsCap(r.subagents_cap)
} catch {
models = []
}
@@ -282,6 +285,9 @@ async function boot() {
onLayout: (on) => {
$("shell").dataset.activity = String(on)
},
here: onThisComputer,
// The conversation as it now stands: the summary's divider, read again, if it is still open.
compacted: (id) => openChatId() === id && openChat(id),
})
$("new-chat-btn").prepend(icon(ICONS.plus))
+42 -14
View File
@@ -123,7 +123,7 @@ export function modelPicker({ id, models, value, onChange, label }) {
return btn
}
// --- How hard a model thinks, and how many agents a chat may have --------------------------------
// --- How hard a model thinks, and how many subagents a chat may have --------------------------------
//
// The levels are the harness spec's (3.2.0, `models/schema.json`), as /api/v2/models gives them:
// `levels` in offering order (`off` first where the model can switch thinking off, then its
@@ -193,28 +193,57 @@ export function effortControl({ model, value, onChange, id, compact }) {
}
/**
* Whether a new chat on `model` may have a team: a chat on the server (an agent chat's team runs
* on its device), a model that calls tools and serves two requests at once (`parallel` null is a
* hosted API with no stated limit). The server decides again; this only keeps the fields away
* where they could never apply. An open chat asks the server (`GET /chats/{chat}/team`).
* Whether a new chat on `model` is offered subagents: the model's `subagents.offered` in `/models`
* (server 2.0.0-beta.7: subagents on for the model and the instance, and at least one model could
* serve as one -- the model itself with room for two requests at once, or a usable model outside
* its one-model-at-a-time server). A server that does not say it: a model that calls tools and
* serves two requests at once (`parallel` null is a hosted API with no stated limit). The server
* decides again; this only keeps the chip away where it could never apply. An open chat has its
* own `subagents` (chat.js).
*/
export function teamPossible(model) {
const said = model?.subagents
if (said !== undefined && said !== null) return typeof said === "object" ? !!said.offered : !!said
return !!model?.tools && (model.parallel === null || model.parallel === undefined || model.parallel >= 2)
}
/** Whether only a model the person names in the chat could serve its subagents (`named_only`). */
export function namedOnly(said) {
return !!(said && typeof said === "object" && said.named_only)
}
// The instance's most, as `/models` said it last (`subagents_cap`); 32 where it says nothing.
export const SUBAGENTS_CAP = 32
let knownCap = null
export function noteSubagentsCap(n) {
knownCap = Number(n) >= 1 ? Number(n) : null
}
/** The most subagents a chat may have: the first of `sources` that says it (`cap`), else the instance's. */
export function subagentsCap(...sources) {
for (const s of sources) {
const n = Number(s?.cap)
if (Number.isFinite(n) && n >= 1) return n
}
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 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.")
/**
* The team's size: the fewest and the most agents, the chat's own agent included, as two number
* fields on one shared track. `value` is { min, max } (empty fields for a new chat: the person's
* default for new chats then stands); `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 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.
*/
export function agentsControl({ value, onChange, id, compact }) {
export function agentsControl({ value, onChange, id, compact, cap = SUBAGENTS_CAP }) {
const field = (which, v, label) =>
h("input", {
type: "number",
class: "agents__input",
id: which === "min" ? id : null,
min: "1",
max: String(cap),
step: "1",
inputmode: "numeric",
value: v === undefined || v === null ? "" : String(v),
@@ -226,8 +255,7 @@ export function agentsControl({ value, onChange, id, compact }) {
const high = field("max", value?.max, t("Most"))
const bounds = () => {
high.min = low.value || "1"
if (high.value) low.max = high.value
else low.removeAttribute("max")
low.max = high.value || String(cap)
}
bounds()
const read = () => {
@@ -244,8 +272,8 @@ export function agentsControl({ value, onChange, id, compact }) {
}
const el = h(
"span",
{ class: `agents${compact ? " agents--compact" : ""}`, role: "group", "aria-label": t("Agents"), title: t("Its own agent included. Above one, it can add members that do the work while it leads them.") },
compact ? h("span", { class: "agents__label", text: t("Agents") }) : null,
{ 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,
+4 -4
View File
@@ -341,7 +341,7 @@ async function drawTask(id) {
fact(t("Last run"), statusChip(task.last_run, task.running)),
fact(t("Model"), modelWords(models, task.model)),
fact(t("Reasoning effort"), h("span", { text: task.effort ? levelWord(task.effort) : t("The model's own") })),
fact(t("Agents"), h("span", { text: task.agents ? t("A team of up to %(n)s", { n: task.agents }) : t("No team") })),
fact(t("Subagents"), h("span", { text: task.agents ? t("Up to %(n)s at once", { n: task.agents }) : t("None") })),
fact(t("Tell me"), h("span", { text: NOTIFY[task.notify]?.() || NOTIFY.matters() })),
fact(t("Runs"), h("span", { text: MODE[task.mode]?.() || MODE.fresh() })),
),
@@ -582,15 +582,15 @@ async function drawForm(task) {
const agents = h(
"select",
{ id: "sched-agents", onchange: (e) => (draft.agents = Number(e.target.value) || null) },
h("option", { value: "", text: t("No team"), selected: !draft.agents }),
Array.from({ length: MAX_AGENTS - 1 }, (_, i) => i + 2).map((n) => h("option", { value: String(n), text: t("A team of up to %(n)s", { n }), selected: draft.agents === n })),
h("option", { value: "", text: t("None"), selected: !draft.agents }),
Array.from({ length: MAX_AGENTS - 1 }, (_, i) => i + 2).map((n) => h("option", { value: String(n), text: t("Up to %(n)s at once", { n }), selected: draft.agents === n })),
)
settings.replaceChildren(
field(t("Tell me"), select("sched-notify", NOTIFY, "notify", () => (settings.querySelector("#sched-notify-hint").textContent = NOTIFY_HINT[draft.notify]())), NOTIFY_HINT[draft.notify]?.() || "", "sched-notify", "sched-notify-hint"),
field(t("Runs"), select("sched-mode", MODE, "mode"), draft.mode === "continue" ? t("Each run sees the ones before it.") : t("Each run starts with nothing but the instruction."), "sched-mode"),
field(t("Model"), modelSelect, t("A busy model is waited for, then the next free one is used."), "sched-model"),
field(t("Reasoning effort"), effort, levels.length ? "" : t("Choose a model to choose its effort."), "sched-effort"),
field(t("Agents"), agents, t("A team works on it together, its own agent included."), "sched-agents"),
field(t("Subagents"), agents, t("How many work on it at once, the run's own agent among them, on the same model."), "sched-agents"),
)
settings.querySelector("#sched-mode").addEventListener("change", drawSettings)
}