4 Commits
Author SHA1 Message Date
HomerandClaude Opus 5.5 10c82a3c7b LLeMbas Desktop 1.0.0-beta.5: CLI 2.0.0-beta.5, the look of LLeMbas 2.0.0-beta.7
ci / check (push) Failing after 3s
ci / start (push) Failing after 3s
release / linux (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 18:40:12 +00:00
HomerandClaude Opus 5.5 3510582c8f Subagents: one slider with two handles, Min and Max; 1 is the agent alone
ci / check (push) Failing after 3s
ci / start (push) Failing after 3s
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 <noreply@anthropic.com>
2026-11-09 18:06:06 +00:00
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
16 changed files with 945 additions and 287 deletions

No files matched your search

+28
View File
@@ -6,6 +6,34 @@ All notable changes to LLeMbas Desktop. The format follows
## [Unreleased] ## [Unreleased]
## [1.0.0-beta.5] — 2026-11-09
Carries LLeMbas CLI 2.0.0-beta.5 and the look of LLeMbas 2.0.0-beta.7, which the Subagents
setting and the panel's compaction need.
### Changed
- **Subagents**, by that name everywhere. The chip under the message box says **Subagents** with 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
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 ## [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 Carries LLeMbas CLI 2.0.0-beta.3 and the look of LLeMbas 2.0.0-beta.6. Scheduled tasks need LLeMbas
+1 -1
View File
@@ -1 +1 @@
2.0.0-beta.3 2.0.0-beta.5
+235 -35
View File
@@ -194,10 +194,8 @@ KEYS = (
"off", "off",
"default", "default",
"Think before answering", "Think before answering",
"Fewest", "Min",
"Most", "Max",
"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…",
"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.", "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", "Back",
"Open the activity panel", "Open the activity panel",
"Close the activity panel", "Close the activity panel",
"1 agent working",
"%(n)s agents working",
"%(n)s queued", "%(n)s queued",
"%(n)s waiting", "%(n)s waiting",
"%(n)s finished", "%(n)s finished",
@@ -240,9 +236,7 @@ KEYS = (
"Estimated: no reply has been counted yet.", "Estimated: no reply has been counted yet.",
"%(n)s tools offered.", "%(n)s tools offered.",
"No files written in this chat yet.", "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.", "Nothing running.",
"Team: %(size)s of %(max)s agents",
"Show %(n)s more", "Show %(n)s more",
"Room", "Room",
"report", "report",
@@ -323,8 +317,6 @@ KEYS = (
"A busy model is waited for, then the next free one is used.", "A busy model is waited for, then the next free one is used.",
"A result, but not a run with nothing to report.", "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 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.", "Choose a model to choose its effort.",
"Due %(when)s", "Due %(when)s",
"Each run sees the ones before it.", "Each run sees the ones before it.",
@@ -333,7 +325,6 @@ KEYS = (
"Edit %(title)s", "Edit %(title)s",
"Every result, even with nothing to report.", "Every result, even with nothing to report.",
"Models for scheduled tasks", "Models for scheduled tasks",
"No team",
"Only in its history.", "Only in its history.",
"Open the chat", "Open the chat",
"Result", "Result",
@@ -358,6 +349,31 @@ KEYS = (
"Open its chat", "Open its chat",
"The model's own", "The model's own",
"run by hand", "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",
"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",
"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 # The app's own sentences. The mode names (Manual, Edit, Auto, Plan) stay English, as the
@@ -365,6 +381,32 @@ KEYS = (
# the device, as the server's catalogue has it. # the device, as the server's catalogue has it.
OWN: dict[str, dict[str, str]] = { OWN: dict[str, dict[str, str]] = {
"cs": { "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.",
"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í",
"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", "Menu": "Nabídka",
"Minimise": "Minimalizovat", "Minimise": "Minimalizovat",
"Maximise": "Maximalizovat", "Maximise": "Maximalizovat",
@@ -532,8 +574,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 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 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 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.", "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", "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í.", "Each run sees the ones before it.": "Každé spuštění vidí ta předchozí.",
@@ -542,7 +582,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "Upravit %(title)s", "Edit %(title)s": "Upravit %(title)s",
"Every result, even with nothing to report.": "Každý výsledek, i když není co hlásit.", "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", "Models for scheduled tasks": "Modely pro naplánované úlohy",
"No team": "Bez týmu",
"Only in its history.": "Jen v její historii.", "Only in its history.": "Jen v její historii.",
"Open the chat": "Otevřít konverzaci", "Open the chat": "Otevřít konverzaci",
"Result": "Výsledek", "Result": "Výsledek",
@@ -563,6 +602,32 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Žádný den nezaškrtnutý: každý den.", "No day ticked: every day.": "Žádný den nezaškrtnutý: každý den.",
}, },
"de": { "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.",
"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",
"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ü", "Menu": "Menü",
"Minimise": "Minimieren", "Minimise": "Minimieren",
"Maximise": "Maximieren", "Maximise": "Maximieren",
@@ -730,8 +795,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 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 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 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.", "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", "Due %(when)s": "Fällig %(when)s",
"Each run sees the ones before it.": "Jeder Lauf sieht die vorherigen.", "Each run sees the ones before it.": "Jeder Lauf sieht die vorherigen.",
@@ -740,7 +803,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "%(title)s bearbeiten", "Edit %(title)s": "%(title)s bearbeiten",
"Every result, even with nothing to report.": "Jedes Ergebnis, auch ohne Neuigkeiten.", "Every result, even with nothing to report.": "Jedes Ergebnis, auch ohne Neuigkeiten.",
"Models for scheduled tasks": "Modelle für geplante Aufgaben", "Models for scheduled tasks": "Modelle für geplante Aufgaben",
"No team": "Kein Team",
"Only in its history.": "Nur in ihrem Verlauf.", "Only in its history.": "Nur in ihrem Verlauf.",
"Open the chat": "Chat öffnen", "Open the chat": "Chat öffnen",
"Result": "Ergebnis", "Result": "Ergebnis",
@@ -761,6 +823,32 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Kein Tag angehakt: jeden Tag.", "No day ticked: every day.": "Kein Tag angehakt: jeden Tag.",
}, },
"es": { "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.",
"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",
"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ú", "Menu": "Menú",
"Minimise": "Minimizar", "Minimise": "Minimizar",
"Maximise": "Maximizar", "Maximise": "Maximizar",
@@ -928,8 +1016,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 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 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 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.", "Choose a model to choose its effort.": "Elija un modelo para elegir su esfuerzo.",
"Due %(when)s": "Prevista %(when)s", "Due %(when)s": "Prevista %(when)s",
"Each run sees the ones before it.": "Cada ejecución ve las anteriores.", "Each run sees the ones before it.": "Cada ejecución ve las anteriores.",
@@ -938,7 +1024,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "Editar %(title)s", "Edit %(title)s": "Editar %(title)s",
"Every result, even with nothing to report.": "Cada resultado, incluso sin nada que informar.", "Every result, even with nothing to report.": "Cada resultado, incluso sin nada que informar.",
"Models for scheduled tasks": "Modelos para tareas programadas", "Models for scheduled tasks": "Modelos para tareas programadas",
"No team": "Sin equipo",
"Only in its history.": "Solo en su historial.", "Only in its history.": "Solo en su historial.",
"Open the chat": "Abrir el chat", "Open the chat": "Abrir el chat",
"Result": "Resultado", "Result": "Resultado",
@@ -959,6 +1044,32 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Ningún día marcado: todos los días.", "No day ticked: every day.": "Ningún día marcado: todos los días.",
}, },
"fr": { "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.",
"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",
"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", "Menu": "Menu",
"Minimise": "Réduire", "Minimise": "Réduire",
"Maximise": "Agrandir", "Maximise": "Agrandir",
@@ -1126,8 +1237,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 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 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 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.", "Choose a model to choose its effort.": "Choisissez un modèle pour choisir son effort.",
"Due %(when)s": "Prévue %(when)s", "Due %(when)s": "Prévue %(when)s",
"Each run sees the ones before it.": "Chaque exécution voit les précédentes.", "Each run sees the ones before it.": "Chaque exécution voit les précédentes.",
@@ -1136,7 +1245,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "Modifier %(title)s", "Edit %(title)s": "Modifier %(title)s",
"Every result, even with nothing to report.": "Chaque résultat, même sans rien à signaler.", "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", "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.", "Only in its history.": "Seulement dans son historique.",
"Open the chat": "Ouvrir la conversation", "Open the chat": "Ouvrir la conversation",
"Result": "Résultat", "Result": "Résultat",
@@ -1157,6 +1265,32 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Aucun jour coché : tous les jours.", "No day ticked: every day.": "Aucun jour coché : tous les jours.",
}, },
"hu": { "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.",
"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",
"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ü", "Menu": "Menü",
"Minimise": "Kis méret", "Minimise": "Kis méret",
"Maximise": "Teljes méret", "Maximise": "Teljes méret",
@@ -1324,8 +1458,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 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 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 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.", "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", "Due %(when)s": "Esedékes: %(when)s",
"Each run sees the ones before it.": "Minden futás látja az előzőeket.", "Each run sees the ones before it.": "Minden futás látja az előzőeket.",
@@ -1334,7 +1466,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "%(title)s szerkesztése", "Edit %(title)s": "%(title)s szerkesztése",
"Every result, even with nothing to report.": "Minden eredmény, akkor is, ha nincs mit jelenteni.", "Every result, even with nothing to report.": "Minden eredmény, akkor is, ha nincs mit jelenteni.",
"Models for scheduled tasks": "Modellek az ütemezett feladatokhoz", "Models for scheduled tasks": "Modellek az ütemezett feladatokhoz",
"No team": "Nincs csapat",
"Only in its history.": "Csak az előzményeiben.", "Only in its history.": "Csak az előzményeiben.",
"Open the chat": "A csevegés megnyitása", "Open the chat": "A csevegés megnyitása",
"Result": "Eredmény", "Result": "Eredmény",
@@ -1355,6 +1486,32 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Ha egy nap sincs bejelölve: minden nap.", "No day ticked: every day.": "Ha egy nap sincs bejelölve: minden nap.",
}, },
"it": { "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.",
"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",
"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", "Menu": "Menu",
"Minimise": "Riduci a icona", "Minimise": "Riduci a icona",
"Maximise": "Ingrandisci", "Maximise": "Ingrandisci",
@@ -1522,8 +1679,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 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 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 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.", "Choose a model to choose its effort.": "Scegli un modello per sceglierne l’impegno.",
"Due %(when)s": "Prevista %(when)s", "Due %(when)s": "Prevista %(when)s",
"Each run sees the ones before it.": "Ogni esecuzione vede quelle precedenti.", "Each run sees the ones before it.": "Ogni esecuzione vede quelle precedenti.",
@@ -1532,7 +1687,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "Modifica %(title)s", "Edit %(title)s": "Modifica %(title)s",
"Every result, even with nothing to report.": "Ogni risultato, anche senza nulla da segnalare.", "Every result, even with nothing to report.": "Ogni risultato, anche senza nulla da segnalare.",
"Models for scheduled tasks": "Modelli per le attività pianificate", "Models for scheduled tasks": "Modelli per le attività pianificate",
"No team": "Nessun team",
"Only in its history.": "Solo nella sua cronologia.", "Only in its history.": "Solo nella sua cronologia.",
"Open the chat": "Apri la chat", "Open the chat": "Apri la chat",
"Result": "Risultato", "Result": "Risultato",
@@ -1553,6 +1707,32 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Nessun giorno spuntato: ogni giorno.", "No day ticked: every day.": "Nessun giorno spuntato: ogni giorno.",
}, },
"pl": { "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.",
"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",
"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", "Menu": "Menu",
"Minimise": "Minimalizuj", "Minimise": "Minimalizuj",
"Maximise": "Maksymalizuj", "Maximise": "Maksymalizuj",
@@ -1720,8 +1900,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 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 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 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.", "Choose a model to choose its effort.": "Wybierz model, aby wybrać jego wysiłek.",
"Due %(when)s": "Zaplanowane na %(when)s", "Due %(when)s": "Zaplanowane na %(when)s",
"Each run sees the ones before it.": "Każde uruchomienie widzi poprzednie.", "Each run sees the ones before it.": "Każde uruchomienie widzi poprzednie.",
@@ -1730,7 +1908,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "Edytuj %(title)s", "Edit %(title)s": "Edytuj %(title)s",
"Every result, even with nothing to report.": "Każdy wynik, nawet bez niczego do zgłoszenia.", "Every result, even with nothing to report.": "Każdy wynik, nawet bez niczego do zgłoszenia.",
"Models for scheduled tasks": "Modele dla zaplanowanych zadań", "Models for scheduled tasks": "Modele dla zaplanowanych zadań",
"No team": "Bez zespołu",
"Only in its history.": "Tylko w jego historii.", "Only in its history.": "Tylko w jego historii.",
"Open the chat": "Otwórz czat", "Open the chat": "Otwórz czat",
"Result": "Wynik", "Result": "Wynik",
@@ -1751,6 +1928,32 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Żaden dzień niezaznaczony: codziennie.", "No day ticked: every day.": "Żaden dzień niezaznaczony: codziennie.",
}, },
"sk": { "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.",
"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",
"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", "Menu": "Ponuka",
"Minimise": "Minimalizovať", "Minimise": "Minimalizovať",
"Maximise": "Maximalizovať", "Maximise": "Maximalizovať",
@@ -1918,8 +2121,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 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 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 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ť.", "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", "Due %(when)s": "Naplánované na %(when)s",
"Each run sees the ones before it.": "Každé spustenie vidí tie predchádzajúce.", "Each run sees the ones before it.": "Každé spustenie vidí tie predchádzajúce.",
@@ -1928,7 +2129,6 @@ OWN: dict[str, dict[str, str]] = {
"Edit %(title)s": "Upraviť %(title)s", "Edit %(title)s": "Upraviť %(title)s",
"Every result, even with nothing to report.": "Každý výsledok, aj keď nie je čo hlásiť.", "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", "Models for scheduled tasks": "Modely pre naplánované úlohy",
"No team": "Bez tímu",
"Only in its history.": "Len v jej histórii.", "Only in its history.": "Len v jej histórii.",
"Open the chat": "Otvoriť konverzáciu", "Open the chat": "Otvoriť konverzáciu",
"Result": "Výsledok", "Result": "Výsledok",
+1 -1
View File
@@ -2085,7 +2085,7 @@ checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]] [[package]]
name = "llembas-desktop" name = "llembas-desktop"
version = "1.0.0-beta.4" version = "1.0.0-beta.5"
dependencies = [ dependencies = [
"libc", "libc",
"serde", "serde",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "llembas-desktop" name = "llembas-desktop"
version = "1.0.0-beta.4" version = "1.0.0-beta.5"
description = "LLeMbas Desktop: the agent on this computer and your LLeMbas server's chats, in an app of its own" description = "LLeMbas Desktop: the agent on this computer and your LLeMbas server's chats, in an app of its own"
authors = ["Jaroslav Beneš"] authors = ["Jaroslav Beneš"]
license = "MIT" license = "MIT"
+3 -3
View File
@@ -1,5 +1,5 @@
{ {
"tag": "v2.0.0-beta.6", "tag": "v2.0.0-beta.7",
"commit": "a71e9b4bdb8acb6e9949e978253a72c347cc67f3", "commit": "e3bd54f4d210cff6adb4586199caed4f41987f5e",
"sha256": "e5f76f542ed5512702e06d69fa1dfa098c511f67dacbd7cc93f4a0618c6ae966" "sha256": "c08adb703678408b469520ee41a0918424cb962ecc89d80f4cbdac5e5c437b92"
} }
+119 -31
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); } 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 { 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 { font: var(--text-sm) / var(--leading-normal) var(--font-body); }
.status.is-ok { color: var(--leaf); } .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); } .check { display: grid; grid-template-columns: var(--check-size) minmax(0, 1fr); gap: var(--sp-2); align-items: center; color: var(--ink); }
@@ -708,26 +709,75 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
} }
.toggle input:checked { background: var(--accent); border-color: var(--accent); } .toggle input:checked { background: var(--accent); border-color: var(--accent); }
.toggle input:checked::after { background: var(--accent-ink); transform: translateX(var(--check-size)); } .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, Min and Max: one range with two handles. Three rows of known height -- the
its word before them -- fixed tracks, so nothing moves when a number grows a digit. */ values said on one line, the track box (one control high), the two ends -- and in the box two
.agents { range inputs laid over one track, the span between the handles filled. The inputs let only
display: inline-grid; their handles take the pointer, so either can be grabbed wherever the other is. */
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); .range2 { display: grid; grid-template-rows: auto var(--control-h) auto; gap: var(--sp-1); width: 100%; }
gap: var(--sp-2); .range2__said { font: 600 var(--text-sm) / var(--leading-tight) var(--font-ui); color: var(--ink); font-variant-numeric: tabular-nums; }
align-items: center; .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%; 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); } .range2__input.is-top { z-index: 1; }
.agents__input { width: 100%; min-width: 0; height: var(--control-h); padding: 0 var(--control-px-sm); } .range2__input::-webkit-slider-runnable-track { height: 100%; background: none; border: 0; }
.agents--compact { .range2__input::-moz-range-track { height: 100%; background: none; border: 0; }
grid-template-columns: auto calc(var(--w-field-xs) / 2) auto calc(var(--w-field-xs) / 2); .range2__input::-webkit-slider-thumb {
width: auto; -webkit-appearance: none;
height: var(--control-h-sm); appearance: none;
font: var(--text-sm) var(--font-ui); 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); } .range2__input::-moz-range-thumb {
.agents__label { color: var(--ink-muted); white-space: nowrap; } 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). */ /* Markdown, drawn by markdown.js from model output (never as HTML). */
.md { overflow-wrap: anywhere; min-width: 0; } .md { overflow-wrap: anywhere; min-width: 0; }
.md > * + * { margin-top: var(--sp-3); } .md > * + * { margin-top: var(--sp-3); }
@@ -920,6 +970,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__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__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); } .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--messages { --ctx: var(--accent); }
.ctx--system { --ctx: var(--ink-muted); } .ctx--system { --ctx: var(--ink-muted); }
.ctx--memory { --ctx: var(--warning); } .ctx--memory { --ctx: var(--warning); }
@@ -943,15 +1001,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 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. */ (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 { 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__goal { margin: 0; color: var(--ink); }
.tasks__list { display: grid; gap: 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__more > summary { cursor: pointer; padding: var(--sp-1) 0; color: var(--ink-muted); } .tasks__list { display: grid; gap: var(--sp-2); }
.tasks__more[open] > summary { margin-bottom: var(--sp-1); } /* 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 { 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 { .agent__row {
display: grid; 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); gap: var(--sp-2);
align-items: center; align-items: center;
min-height: var(--control-h); min-height: var(--control-h);
@@ -960,17 +1044,21 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
list-style: none; list-style: none;
} }
.agent__row::-webkit-details-marker { display: 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__row:hover { background: var(--surface-hover); border-radius: var(--radius-lg); }
.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__what { display: grid; min-width: 0; } .agent__what { display: grid; min-width: 0; }
.agent__name, .agent__role { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .agent__name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--ink); }
.agent__name { color: var(--ink); } .agent.is-done .agent__name, .agent.is-stopped .agent__name { color: var(--ink-muted); }
.agent__role { font-size: var(--text-xs); 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__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 { 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__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); } .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); }
+208 -88
View File
File diff suppressed because it is too large. Load diff
+206 -59
View File
@@ -1,14 +1,18 @@
// The chat's activity panel, beside it: what is in its context, by category, against the model's // The chat's activity panel, beside it, opened from the header's button at any time: where an
// window; the files the chat and its agents wrote; every background agent -- a team's members as // agent chat works; what is in its context, by category, against the model's window, how far it
// a tree, helpers and commands -- with its state, how long it has run, its model, its latest step // is to the automatic compaction and a button to compact now; the files the chat and its
// and what it is writing, its whole conversation read-only and a button to stop it; the team's // subagents wrote; every background task -- subagents, a team's members as a tree under the one
// room; and the plan. The strip above the message box sums the agents up and opens it. The same // that leads them, background commands -- with how long it has run and a stop square, unfolding
// facts as the server's own panel (`services/activity.py`), from /api/v2: // 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?since=&context= everything, with a `version`
// GET /chats/{chat}/activity/events `activity` {version, summary} on each change // 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}/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 // 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 // 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 const CONTEXT_EVERY = 15000
let layout = () => {} // (on) -> the shell makes room for the panel 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 chat = null // the open chat: { id, kind, … }
let stream = null // the events stream's handle let stream = null // the events stream's handle
let retry = 0 // the reconnect timer let retry = 0 // the reconnect timer
@@ -69,12 +75,14 @@ export function compact(value) {
return `${trim(n / 1000000, 2)}M` 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) { export function summaryText(counts) {
if (!counts) return "" if (!counts) return ""
const parts = [] const parts = []
if (counts.working === 1) parts.push(t("1 agent working")) if (counts.working) parts.push(t("%(n)s running", { n: counts.working }))
else if (counts.working) parts.push(t("%(n)s agents working", { n: counts.working }))
if (counts.queued) parts.push(t("%(n)s queued", { n: counts.queued })) 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.waiting) parts.push(t("%(n)s waiting", { n: counts.waiting }))
if (counts.finished) parts.push(t("%(n)s finished", { n: counts.finished })) if (counts.finished) parts.push(t("%(n)s finished", { n: counts.finished }))
@@ -141,9 +149,10 @@ export function watchActivity(next) {
cursor = 0 cursor = 0
readAt = contextAt = 0 readAt = contextAt = 0
view = null view = null
compactError = ""
openRows.clear() openRows.clear()
renderStrip() renderStrip()
$("activity-open")?.setAttribute("aria-expanded", String(panelOpen())) markOpen()
if (!chat) { if (!chat) {
if (panelOpen()) showPanel(false) if (panelOpen()) showPanel(false)
return 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() { function renderStrip() {
const strip = $("activity-strip") const strip = $("activity-strip")
// A chip for every open chat: what runs, while something does, as the server's strip says it (a // Only while something runs, as the server's own line: the header's button is the way into the
// chat whose helpers all finished long ago does not carry "12 finished" for ever: the panel // panel at any other time (and a chat whose tasks all finished long ago does not carry
// still lists them); else only the way into the panel. // "12 finished" for ever: the panel still lists them).
strip.hidden = !chat const active = chat && summary?.active ? summaryText(summary) : ""
if (!chat) return clear(strip) strip.hidden = !active
const active = summary?.active ? summaryText(summary) : "" if (!active) return clear(strip)
const text = active || t("Activity")
strip.replaceChildren( strip.replaceChildren(
h( h(
"button", "button",
{ {
type: "button", type: "button",
id: "activity-open", id: "activity-strip-btn",
class: `strip__btn${active ? " is-active" : ""}`, class: "strip__btn is-active",
"aria-label": `${text} — ${panelOpen() ? t("Close the activity panel") : t("Open the activity panel")}`, "aria-label": `${active} — ${panelOpen() ? t("Close the activity panel") : t("Open the activity panel")}`,
"aria-controls": "activity", "aria-controls": "activity",
"aria-expanded": String(panelOpen()), "aria-expanded": String(panelOpen()),
onclick: togglePanel, onclick: togglePanel,
}, },
h("span", { class: "dot", "aria-hidden": "true" }), h("span", { class: "dot", "aria-hidden": "true" }),
h("span", { class: "strip__text", text }), h("span", { class: "strip__text", text: active }),
icon(ICONS.chevron), 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 ------------------------------------------------------------------------------------------ // --- The panel ------------------------------------------------------------------------------------------
/** Open or close the panel (the header's Activity button, the strip, Escape). */ /** 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 if (on && !chat) return
$("activity").hidden = !on $("activity").hidden = !on
layout(on) layout(on)
$("activity-open")?.setAttribute("aria-expanded", String(on)) markOpen()
clearInterval(ticker) clearInterval(ticker)
ticker = 0 ticker = 0
if (!on) { if (!on) {
view = null view = null
renderStrip()
return return
} }
ticker = setInterval(tick, 1000) ticker = setInterval(tick, 1000)
renderStrip()
render() render()
read(true) read(true)
$("activity-close").focus() $("activity-close").focus()
@@ -359,15 +392,43 @@ function render() {
} }
// Kept where the reader was: the panel is drawn again on every change. // Kept where the reader was: the panel is drawn again on every change.
const scroll = body.scrollTop 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("context", t("Context"), 0, contextPart(data.context)),
section("outputs", t("Outputs"), (data.outputs || []).length, outputsPart(data.outputs || [])), 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, data.plan ? section("plan", t("Plan"), 0, h("div", { class: "activity__plan" }, planList(data.plan))) : null,
) ]
body.replaceChildren(...parts.filter(Boolean))
body.scrollTop = scroll 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) { function contextPart(c) {
if (!c) return h("p", { class: "activity__empty", text: t("Not measured.") }) if (!c) return h("p", { class: "activity__empty", text: t("Not measured.") })
const known = c.percent !== null && c.percent !== undefined const known = c.percent !== null && c.percent !== undefined
@@ -410,7 +471,67 @@ function contextPart(c) {
{ class: "ctx__note" }, { 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 })}`, `${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. */ /** A category's key as a class name: the server's words, kept to letters and underscores. */
@@ -439,31 +560,33 @@ function outputsPart(outputs) {
) )
} }
function tasksPart() { function tasksPart(entries) {
const entries = data.agents || []
const team = data.team const team = data.team
const box = h("div", { class: "tasks", "aria-live": "polite" }) const box = h("div", { class: "tasks", "aria-live": "polite" })
if (!entries.length && !team) { 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 return box
} }
box.append(h("p", { class: "tasks__summary", text: summaryText(data.summary) || t("Nothing running.") }))
if (team) { 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 })) if (team.goal) box.append(h("p", { class: "tasks__goal", text: team.goal }))
} }
const live = 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
const ended = entries.filter((e) => !ACTIVE.includes(e.state)) // 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) { if (live.length) {
box.append(h("div", { class: "tasks__list" }, live.slice(0, SHOWN).map(agentRows))) 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 (live.length > SHOWN) box.append(fold("more", t("Show %(n)s more", { n: live.length - SHOWN }), live.slice(SHOWN)))
} 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
} }
if (ended.length) box.append(fold("finished", t("%(n)s finished", { n: ended.length }), ended))
if (team) { function roomPart() {
box.append(h("h3", { class: "activity__subheading", text: t("Room") })) if (!room.length) return h("p", { class: "activity__empty", text: t("Nothing has been said in the room yet.") })
box.append( return h(
room.length
? h(
"ol", "ol",
{ class: "room" }, { class: "room" },
room.map((m) => room.map((m) =>
@@ -481,22 +604,27 @@ function tasksPart() {
), ),
), ),
) )
: h("p", { class: "activity__empty", text: t("Nothing has been said in the room yet.") }),
)
}
return box
} }
/** A folded list of agents ("Show 3 more", "12 finished"), remembered open across redraws. */ /** A folded list of tasks ("Show 3 more", "Finished 12", "Tasks: 2"), remembered across redraws. */
function fold(name, label, list) { function fold(name, label, list, cls = "") {
const el = h("details", { class: "tasks__more", open: openRows.has(`fold:${name}`) }, h("summary", { text: label }), h("div", { class: "tasks__list" }, list.map(agentRows))) 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}`))) el.addEventListener("toggle", () => (el.open ? openRows.add(`fold:${name}`) : openRows.delete(`fold:${name}`)))
return el 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) { 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) { function agentRow(a) {
@@ -512,7 +640,7 @@ function agentRow(a) {
"button", "button",
{ {
type: "button", type: "button",
class: "pane__btn agent__stop", class: "agent__stop",
"aria-label": t("Stop %(name)s", { name: a.name }), "aria-label": t("Stop %(name)s", { name: a.name }),
title: t("Stop %(name)s", { name: a.name }), title: t("Stop %(name)s", { name: a.name }),
onclick: (e) => { onclick: (e) => {
@@ -521,13 +649,15 @@ function agentRow(a) {
stopAgent(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( const facts = h(
"p", "p",
{ class: "agent__facts" }, { class: "agent__facts" },
h("span", { class: "agent__state", text: stateWord(a.state) }), 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.agent ? h("span", { text: a.agent }) : null,
a.model ? h("span", { class: "agent__model", text: a.model }) : 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, 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( h(
"summary", "summary",
{ class: "agent__row" }, { class: "agent__row" },
h("span", { class: "agent__dot", "aria-hidden": "true" }), h("span", { class: "agent__chev", "aria-hidden": "true" }, icon(ICONS.chevron)),
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__what", title: label(a) }, h("bdi", { class: "agent__name", text: label(a) })),
time, time,
stopBtn, stopBtn,
), ),
@@ -556,12 +686,11 @@ function agentRow(a) {
"div", "div",
{ class: "agent__body" }, { class: "agent__body" },
facts, 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, a.brief ? h("p", { class: "agent__brief", text: a.brief }) : null,
step, step,
a.text ? h("p", { class: "agent__text", text: a.text }) : null, a.text ? h("p", { class: "agent__text", text: a.text }) : null,
a.transcript a.transcript ? h("button", { type: "button", class: "btn btn--sm", text: t("Open its conversation"), onclick: () => openAgent(a) }) : null,
? 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)))) el.style.setProperty("--depth", String(Math.max(0, Math.min(6, Number(a.depth) || 0))))
@@ -569,6 +698,22 @@ function agentRow(a) {
return el 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) { async function stopAgent(a) {
if (!chat) return if (!chat) return
if (a.kind === "member") { if (a.kind === "member") {
@@ -667,8 +812,10 @@ async function readAgent() {
// --- Start ------------------------------------------------------------------------------------------ // --- Start ------------------------------------------------------------------------------------------
export function initActivity({ onLayout }) { export function initActivity({ onLayout, here, compacted }) {
layout = onLayout || layout layout = onLayout || layout
onThisComputer = here || onThisComputer
onCompacted = compacted || onCompacted
$("activity-close").replaceChildren(icon(ICONS.close)) $("activity-close").replaceChildren(icon(ICONS.close))
$("activity-close").addEventListener("click", () => showPanel(false)) $("activity-close").addEventListener("click", () => showPanel(false))
$("activity-back").replaceChildren(icon(ICONS.back)) $("activity-back").replaceChildren(icon(ICONS.back))
+35 -18
View File
@@ -13,9 +13,9 @@ import { markdown } from "./markdown.js"
import { openViewer, showKind } from "./viewer.js" import { openViewer, showKind } from "./viewer.js"
import { readLocal } from "./explorer.js" import { readLocal } from "./explorer.js"
import { menu, ask, showLine, popover, closePopover } from "./popup.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 { 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 { agentBlock, agentReady, agentState, onAgentChange } from "./agent.js"
import { folderName } from "./list.js" import { folderName } from "./list.js"
@@ -30,7 +30,7 @@ export function openChatId() {
// --- Reading ----------------------------------------------------------------------------------- // --- Reading -----------------------------------------------------------------------------------
function onThisComputer(chat) { export function onThisComputer(chat) {
return !!chat?.device && ctx.thisDevices().includes(chat.device.token_id) 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))) 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) { 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 { 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 { } catch {
return false return null
} }
} }
@@ -640,26 +651,31 @@ 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 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 } = {}) {
const said = value?.min || value?.max ? `${value.min ?? "…"}–${value.max ?? "…"}` : t("default") const said = value?.min || value?.max ? `${value.min ?? "…"}–${value.max ?? "…"}` : t("default")
return chip({ return chip({
ico: ICONS.users, ico: ICONS.users,
label: `${t("Agents")} ${said}`, label: `${t("Subagents")} ${said}`,
title: t("Its own agent included. Above one, it can add members that do the work while it leads them."), title: own ? SUBAGENTS_OWN() : SUBAGENTS_HINT(),
cls: `cchip--subagents${own ? " is-warn" : ""}`,
onclick: (e) => { onclick: (e) => {
const fields = agentsControl({ value, id: "agents-min" }) const fields = agentsControl({ value, id: "agents-min", cap })
popover( popover(
e.currentTarget, e.currentTarget,
h( h(
"div", "div",
{ class: "pop__body" }, { class: "pop__body" },
h("label", { class: "pop__title", for: "agents-min", text: t("Agents") }), h("p", { class: "pop__title", text: t("Subagents") }),
fields, 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)) })), 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 +721,7 @@ async function renderChips() {
renderChips() renderChips()
}, mayPick)) }, mayPick))
if (model) parts.push(effortChip(model, draft.effort, (l) => ((draft.effort = l), renderChips()))) 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)) box.replaceChildren(...parts.filter(Boolean))
return return
} }
@@ -732,8 +748,9 @@ async function renderChips() {
if (chat.model) parts.push(modelChip(models, chat.model, (id) => save({ model: id }), mayPick)) 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 }))) parts.push(effortChip(current, chat.effort || levelsOf(current).start, (l) => save({ effort: l })))
box.replaceChildren(...parts.filter(Boolean)) box.replaceChildren(...parts.filter(Boolean))
if (!(await teamOffered(chat)) || drawing !== chipsDrawn || open?.chat?.id !== chat.id) return const offer = await teamOffered(chat)
box.append(agentsChip(open.chat.agents || chat.agents, (a) => save({ agents: a }))) 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", 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", bolt: "M9.5 1L3 9.25h4.25L6.5 15 13 6.75H8.75z",
stop: "M4.5 4.5h7v7h-7z", 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", 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", 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", 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 { initViewer, onViewer, closeViewer } from "./viewer.js"
import { initExplorer, showLocal, showFilesTab } from "./explorer.js" import { initExplorer, showLocal, showFilesTab } from "./explorer.js"
import { initList, refreshList, markCurrent } from "./list.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 { initAttach, allowAttach } from "./attach.js"
import { initPalette, openPalette } from "./palette.js" import { initPalette, openPalette } from "./palette.js"
import { initActivity, showPanel, togglePanel, panelOpen } from "./activity.js" import { initActivity, showPanel, togglePanel, panelOpen } from "./activity.js"
import { initAgent, link } from "./agent.js" import { initAgent, link } from "./agent.js"
import { initScheduled, showScheduled, scheduledCommands } from "./scheduled.js" import { initScheduled, showScheduled, scheduledCommands } from "./scheduled.js"
import { closePopover } from "./popup.js" import { closePopover } from "./popup.js"
import { noteSubagentsCap } from "./models.js"
let status = null let status = null
let view = "" // "setup" | "app" let view = "" // "setup" | "app"
@@ -118,7 +119,9 @@ function stopApp() {
async function loadModels() { async function loadModels() {
try { try {
models = (await get("/models")).models || [] const r = await get("/models")
models = r.models || []
noteSubagentsCap(r.subagents_cap)
} catch { } catch {
models = [] models = []
} }
@@ -282,6 +285,9 @@ async function boot() {
onLayout: (on) => { onLayout: (on) => {
$("shell").dataset.activity = String(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)) $("new-chat-btn").prepend(icon(ICONS.plus))
+92 -41
View File
@@ -123,7 +123,7 @@ export function modelPicker({ id, models, value, onChange, label }) {
return btn 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: // 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 // `levels` in offering order (`off` first where the model can switch thinking off, then its
@@ -193,69 +193,120 @@ 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 * Whether a new chat on `model` is offered subagents: the model's `subagents.offered` in `/models`
* on its device), a model that calls tools and serves two requests at once (`parallel` null is a * (server 2.0.0-beta.7: subagents on for the model and the instance, and at least one model could
* hosted API with no stated limit). The server decides again; this only keeps the fields away * serve as one -- the model itself with room for two requests at once, or a usable model outside
* where they could never apply. An open chat asks the server (`GET /chats/{chat}/team`). * 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) { 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) 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
}
// 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 team's size: the fewest and the most agents, the chat's own agent included, as two number * The agents a chat may run, its own agent counted: one range from 1 to `cap` with two handles,
* fields on one shared track. `value` is { min, max } (empty fields for a new chat: the person's * Min and Max -- two range inputs overlaid on one track, the span between them filled. Each is a
* default for new chats then stands); `onChange({ min, max })` once a field is changed, with the * real slider (arrow keys, Home/End, Page Up/Down), and Min never passes Max. `value` is
* empty one left out. The server checks the pair and says the bounds when it refuses one. * { 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 }) { export function agentsControl({ value, onChange, id, cap = SUBAGENTS_CAP }) {
const field = (which, v, label) => 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", { h("input", {
type: "number", type: "range",
class: "agents__input", class: `range2__input range2__input--${which}`,
id: which === "min" ? id : null, id: which === "min" ? id : null,
min: "1", min: "1",
max: String(top),
step: "1", step: "1",
inputmode: "numeric",
value: v === undefined || v === null ? "" : String(v),
placeholder: compact ? null : t("default"),
"aria-label": label, "aria-label": label,
title: label, title: label,
}) })
const low = field("min", value?.min, t("Fewest")) const low = handle("min", t("Min"))
const high = field("max", value?.max, t("Most")) const high = handle("max", t("Max"))
const bounds = () => { const said = h("output", { class: "range2__said", "aria-live": "polite", for: id || null })
high.min = low.value || "1" const fill = h("span", { class: "range2__fill", "aria-hidden": "true" })
if (high.value) low.max = high.value const box = h("span", { class: "range2__box" }, h("span", { class: "range2__track", "aria-hidden": "true" }, fill), low, high)
else low.removeAttribute("max") 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 set = (v) => {
const read = () => { const hi = clamp(v?.max, Math.min(AGENTS_DEFAULT.max, top))
const out = {} const lo = Math.min(hi, clamp(v?.min, AGENTS_DEFAULT.min))
if (low.value) out.min = Number(low.value) high.value = String(hi)
if (high.value) out.max = Number(high.value) low.value = String(lo)
return out draw()
} }
for (const input of [low, high]) { low.addEventListener("input", () => {
input.addEventListener("change", () => { if (Number(low.value) > Number(high.value)) low.value = high.value
bounds() touched = true
onChange?.(read()) 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( const el = h(
"span", "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.") }, { class: "range2", role: "group", "aria-label": t("Subagents") },
compact ? h("span", { class: "agents__label", text: t("Agents") }) : null, said,
low, box,
h("span", { class: "agents__dash", "aria-hidden": "true", text: "–" }), h("span", { class: "range2__ends", "aria-hidden": "true" }, h("span", { text: "1" }), h("span", { text: String(top) })),
high,
) )
Object.defineProperty(el, "value", { Object.defineProperty(el, "value", {
get: read, get: read,
set: (v) => { set: (v) => {
low.value = v?.min ?? "" given = v || {}
high.value = v?.max ?? "" touched = false
bounds() set(given)
}, },
}) })
return el return el
+4 -4
View File
@@ -341,7 +341,7 @@ async function drawTask(id) {
fact(t("Last run"), statusChip(task.last_run, task.running)), fact(t("Last run"), statusChip(task.last_run, task.running)),
fact(t("Model"), modelWords(models, task.model)), 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("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("Tell me"), h("span", { text: NOTIFY[task.notify]?.() || NOTIFY.matters() })),
fact(t("Runs"), h("span", { text: MODE[task.mode]?.() || MODE.fresh() })), fact(t("Runs"), h("span", { text: MODE[task.mode]?.() || MODE.fresh() })),
), ),
@@ -582,15 +582,15 @@ async function drawForm(task) {
const agents = h( const agents = h(
"select", "select",
{ id: "sched-agents", onchange: (e) => (draft.agents = Number(e.target.value) || null) }, { id: "sched-agents", onchange: (e) => (draft.agents = Number(e.target.value) || null) },
h("option", { value: "", text: t("No team"), selected: !draft.agents }), 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("A team of up to %(n)s", { n }), selected: draft.agents === n })), 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( 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("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("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("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("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) settings.querySelector("#sched-mode").addEventListener("change", drawSettings)
} }
+1 -1
View File
@@ -1,4 +1,4 @@
/* LLeMbas v2.0.0-beta.6 (a71e9b4bdb8a) static/css/tokens.css, copied by scripts/fetch-tokens.sh: do not edit. /* LLeMbas v2.0.0-beta.7 (e3bd54f4d210) static/css/tokens.css, copied by scripts/fetch-tokens.sh: do not edit.
The font-face rules are left out; the app bundles no font. */ The font-face rules are left out; the app bundles no font. */
/* /*
Design tokens. Design tokens.
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"tag": "v2.0.0-beta.6", "tag": "v2.0.0-beta.7",
"commit": "a71e9b4bdb8acb6e9949e978253a72c347cc67f3", "commit": "e3bd54f4d210cff6adb4586199caed4f41987f5e",
"files": { "files": {
"cm-autocomplete.mjs": {"server": "26fe431a5f54e29952e094c93151f11473a80677bfb339711ecca08195a89df4", "copy": "fcb7fcbe4e2b072d031520ec898c81ac3f2f05fb4126e03003c9b5e60ffaa8de"}, "cm-autocomplete.mjs": {"server": "26fe431a5f54e29952e094c93151f11473a80677bfb339711ecca08195a89df4", "copy": "fcb7fcbe4e2b072d031520ec898c81ac3f2f05fb4126e03003c9b5e60ffaa8de"},
"cm-codemirror.mjs": {"server": "dea86a106e13c9316dfedfd29209780b853bdb2b477edf3f7c90f497b592a223", "copy": "0b80ae1289a1dc3e6718727e78bc757caad657e907d0351597867d4f90c36bd2"}, "cm-codemirror.mjs": {"server": "dea86a106e13c9316dfedfd29209780b853bdb2b477edf3f7c90f497b592a223", "copy": "0b80ae1289a1dc3e6718727e78bc757caad657e907d0351597867d4f90c36bd2"},