diff --git a/CHANGELOG.md b/CHANGELOG.md index ba9372b..cd89435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,19 @@ All notable changes to LLeMbas Desktop. The format follows speed), running a tool and what it runs, waiting for you, waiting for subagents, compacting, retrying (counting down), stopped or failed -- each with how long it has lasted, kept moving every second. From a server that sends no such line (before 2.0.0-beta.8), the app shows the - server's own words as before. Translations come with the server's. + server's own words as before. In every language the server has. +- **Subagents, the whole tree** (harness spec 3.4.0, needs the server's 2.0.0-beta.8). The + activity panel shows the main agent and every subagent under the one it reports to: its name and + id, its position, what it is doing right now, how full its context is, its state, model and + mode (with what it may ask for), its messages in the room and the last one, how many slots are + still open, and the ones that ended -- dimmed, with how they ended. Any subagent's conversation + opens read-only, live while it works and after it ended, and so do its findings; a working one + can be stopped (it and those under it). An agent chat's tree comes from its computer; while that + computer is away the panel shows the tree as it last was and says the rest is kept there. +- **Subagents for agent chats.** A new agent chat offers the Subagents setting where the server + says this computer can run them, and an open agent chat where the server offers it for that + chat. +- **A subagent's approval names it**: "Subagent parser (s1), reviews the parser, asks:". - `tests/ui/`: the page under a headless browser against a stand-in for the app's bridge, with the server's answers as a table (`node tests/ui/run.mjs`, needs Playwright and a Chromium). CI runs it on every push (the `ui` job), with Playwright and its Chromium installed for the run. diff --git a/scripts/fetch-fixtures.sh b/scripts/fetch-fixtures.sh new file mode 100755 index 0000000..5468f93 --- /dev/null +++ b/scripts/fetch-fixtures.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Copy the server's /api/v2 contract fixtures that tests/ui reads into tests/ui/fixtures/: +# +# scripts/fetch-fixtures.sh (a tag such as v2.0.0-beta.8, or a branch) +# +# The server is a git checkout, LLeMbas beside this repository by default (LEMBAS_SERVER names +# another); the files are read from the ref, not from its working tree. SOURCE records the ref and +# the commit, so the tests say which server they hold the app to. CI has no server checkout: the +# copies are committed. +set -euo pipefail +cd "$(dirname "$0")/.." + +SERVER="${LEMBAS_SERVER:-../LLeMbas}" +FROM="tests/fixtures/api_v2" +OUT="tests/ui/fixtures" +FILES="chat_agent.json event_ask.json event_status.json event_status_idle.json event_status_person.json +event_status_stopped.json event_tree.json models_agent.json subagent_findings.json subagent_transcript.json tree.json" + +[ $# -eq 1 ] || { echo "usage: scripts/fetch-fixtures.sh " >&2; exit 2; } +commit="$(git -C "$SERVER" rev-parse --verify --quiet "$1^{commit}")" \ + || { echo "fetch-fixtures: $1 is not a commit in $SERVER" >&2; exit 1; } +mkdir -p "$OUT" +for f in $FILES; do + git -C "$SERVER" show "$commit:$FROM/$f" > "$OUT/$f" || { echo "fetch-fixtures: $f is not at $1" >&2; exit 1; } +done +printf 'LLeMbas %s (%s) %s\n' "$1" "$commit" "$FROM" > "$OUT/SOURCE" +echo "fetch-fixtures: $(echo $FILES | wc -w) files from LLeMbas $1 (${commit:0:12}) -> $OUT" diff --git a/scripts/gen-i18n.py b/scripts/gen-i18n.py index f3b8a21..b65ae79 100755 --- a/scripts/gen-i18n.py +++ b/scripts/gen-i18n.py @@ -399,42 +399,52 @@ KEYS = ( "Put the last command and its output into the message box", "Sign in again: the server no longer accepts this app's sign-in.", "The terminal needs a newer LLeMbas server. Ask its administrator to update it.", -) - -# The progress line's sentences (ui/js/progress.js; harness spec 3.4.0, `loop.json` -# progress.texts, its `{{field}}` written as `%(field)s`). Shared with every client, so they -# are the server's to translate and never in OWN: until its catalogues have one, that sentence -# shows in English and this script says how many are waiting -- not a failure. -SERVER_PENDING = ( + # progress.js (harness spec 3.4.0: loop.json progress.texts, keyed as the spec writes them) "Queued", - "Queued: %(ahead)s ahead", - "Waiting for %(model)s: %(reason)s", - "Loading %(model)s", - "Reading the prompt: %(done)s / %(total)s tokens (%(percent)s%%)", + "Queued: {{ahead}} ahead", + "Waiting for {{model}}: {{reason}}", + "Loading {{model}}", + "Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)", "Reading the prompt", - "Thinking", - "Writing: %(rate)s tok/s", + "Writing: {{rate}} tok/s", "Writing", - "Running %(tool)s: %(what)s", - "Running %(tool)s", - "Waiting for you: %(what)s", - "Waiting for subagents: %(running)s running", + "Running {{tool}}: {{what}}", + "Running {{tool}}", + "Waiting for you: {{what}}", + "Waiting for subagents: {{running}} running", "Compacting the conversation", - "Retrying in %(in)s s: %(reason)s", + "Retrying in {{in}} s: {{reason}}", "Stopped", - "Stopped: %(reason)s", - "Failed: %(reason)s", + "Stopped: {{reason}}", + "Failed: {{reason}}", "it serves one request at a time and is answering another", "all its slots are in use", "its server holds another model now", - "allow %(tool)s?", + "allow {{tool}}?", "an approval", "a question", "the plan", "by you", "step limit", "budget", - "%(text)s · %(elapsed)s", + # tree.js, activity.js (the subagent tree), chat.js (a subagent's approval) + "waiting for a slot", + "finished", + "idle", + "paused: context full", + "out of context", + "the main agent", + "%(size)s of %(max)s", + "Open slots: %(n)s", + "They run on the computer this chat runs on.", + "Ended", + "Context used", + "messages: %(n)s", + "findings: %(n)s", + "Findings", + "Stop %(name)s and the subagents under it?", + "This device is not linked right now.", + "Subagent %(name)s (%(id)s), %(position)s, asks:", ) # The app's own sentences. The mode names (Manual, Edit, Auto, Plan) stay English, as the @@ -2254,20 +2264,6 @@ def main() -> None: ours = OWN.get(lang, {}) table: dict[str, str] = {} from_server = 0 - waiting = 0 - for key in SERVER_PENDING: - if key in KEYS: - continue - text = theirs.get(key) - if not text: - waiting += 1 - elif set(FIELD.findall(text)) != set(FIELD.findall(key)): - print(f"gen-i18n: {lang}: the fields of {text!r} do not match {key!r}", file=sys.stderr) - problems += 1 - else: - table[key] = text - if waiting: - print(f"gen-i18n: {lang}: {waiting} progress sentences wait for the server's catalogue (English until then)") for key in KEYS: text = theirs.get(key) if text: diff --git a/tests/ui/fixtures/SOURCE b/tests/ui/fixtures/SOURCE new file mode 100644 index 0000000..d0d1c3d --- /dev/null +++ b/tests/ui/fixtures/SOURCE @@ -0,0 +1 @@ +LLeMbas origin/dev (5d7b79c5c9dc0d9c3df9a8b401c02fdd67927c2d) tests/fixtures/api_v2 diff --git a/tests/ui/fixtures/chat_agent.json b/tests/ui/fixtures/chat_agent.json new file mode 100644 index 0000000..efc07d4 --- /dev/null +++ b/tests/ui/fixtures/chat_agent.json @@ -0,0 +1,31 @@ +{ + "id": "chat2", + "kind": "agent", + "title": "Waybread", + "model": "qwen", + "folder_id": null, + "pinned": false, + "archived": false, + "temporary": false, + "unread": true, + "running": true, + "mode": "edit", + "effort": "low", + "device": { + "token_id": "token1", + "directory": "/home/dev/project" + }, + "device_session_id": "ses_01waybread", + "parent_chat_id": null, + "agents": { + "min": 1, + "max": 4 + }, + "subagents": { + "offered": true, + "named_only": false, + "cap": 32 + }, + "created_at": "2026-01-02T03:04:05+00:00", + "updated_at": "2026-01-02T03:04:05+00:00" +} diff --git a/tests/ui/fixtures/event_ask.json b/tests/ui/fixtures/event_ask.json new file mode 100644 index 0000000..851bad4 --- /dev/null +++ b/tests/ui/fixtures/event_ask.json @@ -0,0 +1,162 @@ +{ + "cards": [ + { + "id": "card1", + "kind": "question", + "why": "", + "dismissable": false, + "expires_in": null, + "items": [ + { + "index": 0, + "key": "q0", + "kind": "question", + "tool": "ask_user", + "title": "Honey or not?", + "header": "Taste", + "detail": "", + "reason": "", + "purpose": "", + "options": [ + { + "label": "Honey", + "description": "", + "recommended": true + }, + { + "label": "Plain", + "description": "As the elves have it", + "recommended": false + } + ], + "multiple": false, + "free_text": true, + "editable": false, + "plan": null, + "mode_choice": true, + "job": "", + "subagent": null + } + ] + }, + { + "id": "card2", + "kind": "approval", + "why": "", + "dismissable": false, + "expires_in": null, + "items": [ + { + "index": 0, + "key": "0", + "kind": "approval", + "tool": "bash", + "title": "Run a command", + "header": "", + "detail": "rm -rf build", + "reason": "Edit mode asks before anything that runs a command.", + "purpose": "Start clean", + "options": [], + "multiple": false, + "free_text": false, + "editable": true, + "plan": null, + "mode_choice": true, + "job": "explore (job1)", + "subagent": null + } + ] + }, + { + "id": "card3", + "kind": "plan", + "why": "", + "dismissable": true, + "expires_in": null, + "items": [ + { + "index": 0, + "key": "p0", + "kind": "plan", + "tool": "plan_submit", + "title": "Bake lembas", + "header": "", + "detail": "", + "reason": "", + "purpose": "", + "options": [], + "multiple": false, + "free_text": true, + "editable": false, + "plan": { + "version": 2, + "title": "Bake lembas", + "summary": "", + "findings": [], + "objectives": [], + "phases": [ + { + "id": "p1", + "title": "Plan", + "status": "pending", + "tasks": [ + { + "id": "t1", + "text": "Gather mallorn leaves", + "status": "todo", + "note": "" + }, + { + "id": "t2", + "text": "Bake slowly", + "status": "todo", + "note": "" + } + ] + } + ], + "steps": [ + "Gather mallorn leaves", + "Bake slowly" + ] + }, + "mode_choice": true, + "job": "", + "subagent": null + } + ] + }, + { + "id": "card4", + "kind": "approval", + "why": "", + "dismissable": false, + "expires_in": null, + "items": [ + { + "index": 0, + "key": "0", + "kind": "approval", + "tool": "bash", + "title": "Run a command", + "header": "", + "detail": "git push", + "reason": "Edit mode asks before anything that runs a command.", + "purpose": "", + "options": [], + "multiple": false, + "free_text": false, + "editable": true, + "plan": null, + "mode_choice": true, + "job": "", + "subagent": { + "id": "s1", + "name": "parser", + "position": "reviews the parser" + } + } + ] + } + ] +} diff --git a/tests/ui/fixtures/event_status.json b/tests/ui/fixtures/event_status.json new file mode 100644 index 0000000..71d2400 --- /dev/null +++ b/tests/ui/fixtures/event_status.json @@ -0,0 +1,9 @@ +{ + "text": "Running bake --slow", + "progress": { + "phase": "running_tool", + "since": 1767323045000, + "tool": "bash", + "what": "bake --slow" + } +} diff --git a/tests/ui/fixtures/event_status_idle.json b/tests/ui/fixtures/event_status_idle.json new file mode 100644 index 0000000..4f6faff --- /dev/null +++ b/tests/ui/fixtures/event_status_idle.json @@ -0,0 +1,4 @@ +{ + "text": "", + "progress": null +} diff --git a/tests/ui/fixtures/event_status_person.json b/tests/ui/fixtures/event_status_person.json new file mode 100644 index 0000000..5e4614d --- /dev/null +++ b/tests/ui/fixtures/event_status_person.json @@ -0,0 +1,8 @@ +{ + "text": "Waiting for you to allow bash…", + "progress": { + "phase": "waiting_for_person", + "since": 1767323045000, + "what": "question" + } +} diff --git a/tests/ui/fixtures/event_status_stopped.json b/tests/ui/fixtures/event_status_stopped.json new file mode 100644 index 0000000..f275abf --- /dev/null +++ b/tests/ui/fixtures/event_status_stopped.json @@ -0,0 +1,8 @@ +{ + "text": "", + "progress": { + "phase": "stopped", + "since": 1767323045000, + "reason": "person" + } +} diff --git a/tests/ui/fixtures/event_tree.json b/tests/ui/fixtures/event_tree.json new file mode 100644 index 0000000..0928efb --- /dev/null +++ b/tests/ui/fixtures/event_tree.json @@ -0,0 +1,75 @@ +{ + "tree": { + "min": 1, + "max": 4, + "size": 2, + "open_slots": 1, + "agents": [ + { + "id": "lead", + "name": "lead", + "position": "the main agent", + "parent": null, + "state": "working", + "model": "qwen-27b", + "mode": "edit", + "request": [], + "manager": true, + "context": { + "used": 41200, + "window": 131072 + }, + "progress": { + "phase": "thinking", + "since": 1767323045000 + }, + "findings": 0, + "messages": 2 + }, + { + "id": "s2", + "name": "parser", + "position": "reviews the parser", + "parent": "lead", + "state": "working", + "model": "qwen-27b", + "mode": "manual", + "task": "Review src/parse.ts.", + "request": [ + "write" + ], + "manager": false, + "context": { + "used": 9100, + "window": 131072 + }, + "progress": { + "phase": "running_tool", + "since": 1767323045000, + "tool": "read", + "what": "src/parse.ts" + }, + "findings": 2, + "messages": 3 + } + ], + "ended": [ + { + "id": "s1", + "name": "scout", + "position": "maps the code", + "parent": "lead", + "state": "done", + "model": "qwen-27b", + "mode": "edit", + "task": "Map it.", + "request": [], + "manager": false, + "progress": null, + "findings": 1, + "messages": 1, + "ended": 1767323045000 + } + ] + } +} diff --git a/tests/ui/fixtures/models_agent.json b/tests/ui/fixtures/models_agent.json new file mode 100644 index 0000000..2d8e087 --- /dev/null +++ b/tests/ui/fixtures/models_agent.json @@ -0,0 +1,81 @@ +{ + "models": [ + { + "id": "qwen", + "name": "Qwen", + "vision": false, + "tools": true, + "efforts": [ + "medium" + ], + "thinking_switch": true, + "levels": [ + "off", + "medium" + ], + "effort": "medium", + "control": "switch", + "parallel": 2, + "context": 131072, + "image": "/api/v2/models/images/0a1b2c3d4e5f.png", + "default": true, + "subagents": { + "offered": true, + "named_only": false + } + }, + { + "id": "gpt-oss", + "name": "gpt-oss", + "vision": false, + "tools": true, + "efforts": [ + "low", + "medium", + "high" + ], + "thinking_switch": false, + "levels": [ + "low", + "medium", + "high" + ], + "effort": "high", + "control": "slider", + "parallel": 1, + "context": 0, + "image": null, + "default": false, + "subagents": { + "offered": true, + "named_only": false + } + }, + { + "id": "vision-small", + "name": "Vision", + "vision": true, + "tools": false, + "efforts": [], + "thinking_switch": false, + "levels": [ + "default" + ], + "effort": "default", + "control": "none", + "parallel": null, + "context": 8192, + "image": null, + "default": false, + "subagents": { + "offered": true, + "named_only": false + } + } + ], + "subagents_cap": 32, + "agent_subagents": { + "offered": true, + "device_runs": true + } +} diff --git a/tests/ui/fixtures/subagent_findings.json b/tests/ui/fixtures/subagent_findings.json new file mode 100644 index 0000000..84f17a5 --- /dev/null +++ b/tests/ui/fixtures/subagent_findings.json @@ -0,0 +1,10 @@ +{ + "id": "s2", + "findings": [ + { + "n": 1, + "text": "Login is POST /session.", + "time": 1767323045000 + } + ] +} diff --git a/tests/ui/fixtures/subagent_transcript.json b/tests/ui/fixtures/subagent_transcript.json new file mode 100644 index 0000000..abe7201 --- /dev/null +++ b/tests/ui/fixtures/subagent_transcript.json @@ -0,0 +1,24 @@ +{ + "id": "s2", + "state": "working", + "turns": [ + { + "role": "user", + "text": "Write the routes.", + "machine": true + }, + { + "role": "assistant", + "text": "Reading the API first.", + "tools": [ + { + "name": "read", + "title": "src/api.py", + "isError": false + } + ], + "error": "", + "running": true + } + ] +} diff --git a/tests/ui/fixtures/tree.json b/tests/ui/fixtures/tree.json new file mode 100644 index 0000000..fa36b92 --- /dev/null +++ b/tests/ui/fixtures/tree.json @@ -0,0 +1,112 @@ +{ + "tree": { + "min": 2, + "max": 4, + "size": 3, + "open_slots": 0, + "agents": [ + { + "id": "lead", + "name": "lead", + "position": "the main agent", + "parent": null, + "state": "working", + "model": "local/qwen", + "mode": "auto", + "request": [], + "manager": true, + "progress": { + "phase": "waiting_for_subagents", + "since": 1767323045000, + "running": 2 + }, + "findings": 0, + "messages": 1 + }, + { + "id": "s1", + "name": "area", + "position": "owns the API", + "parent": "lead", + "state": "working", + "model": "local/qwen", + "mode": "auto", + "request": [], + "manager": true, + "task": "Direct it.", + "progress": null, + "findings": 0, + "messages": 1 + }, + { + "id": "s2", + "name": "routes", + "position": "writes the routes", + "parent": "area", + "state": "working", + "model": "local/qwen", + "mode": "edit", + "request": [ + "execute" + ], + "manager": false, + "task": "Write the routes.", + "context": { + "used": 9100, + "window": 131072 + }, + "progress": { + "phase": "running_tool", + "since": 1767323045000, + "tool": "read", + "what": "src/api.py" + }, + "findings": 1, + "messages": 1 + } + ], + "ended": [ + { + "id": "s3", + "name": "scout", + "position": "maps the code", + "parent": "lead", + "state": "done", + "model": "local/qwen", + "mode": "auto", + "request": [], + "manager": false, + "task": "Map it.", + "progress": null, + "findings": 0, + "messages": 1, + "ended": 1767323045000 + } + ] + }, + "room": [ + { + "n": 1, + "from": "routes", + "to": [ + "area" + ], + "text": "Routes are in.", + "report": false, + "kind": "message", + "time": 1767323045000 + }, + { + "n": 2, + "from": "scout", + "to": [ + "lead" + ], + "text": "Mapped: see map.md.", + "report": true, + "kind": "report", + "time": 1767323045000 + } + ], + "live": true +} diff --git a/tests/ui/run.mjs b/tests/ui/run.mjs index 548dade..87233b2 100644 --- a/tests/ui/run.mjs +++ b/tests/ui/run.mjs @@ -50,6 +50,9 @@ async function launch() { throw new Error("tests/ui: no Chromium found (set CHROMIUM)") } +/** One of the server's /api/v2 contract fixtures, as copied by scripts/fetch-fixtures.sh. */ +const fixture = (name) => JSON.parse(readFileSync(join(HERE, "fixtures", `${name}.json`), "utf8")) + const TYPES = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".mjs": "text/javascript", ".json": "application/json" } // --- The server, as the tests' table ----------------------------------------------------------- @@ -636,9 +639,14 @@ test("B1: a reply being written draws the stream's progress, its clock moving, a const p = await page(agentChatOpen([["status", { text: "Writing…" }]])) await openChatRow(p, "chat2") await p.waitForSelector(".msg.is-live .msg__status") - await p.evaluate((path) => { - window.__mock.frame(path, "status", { text: "Running bake --slow", progress: { phase: "running_tool", since: Date.now() - 65000, tool: "bash", what: "bake --slow" } }) - }, STREAM2) + // An older server sends no progress at all: its status text, as before. + await p.evaluate((path) => window.__mock.frame(path, "status", { text: "Running bake --slow" }), STREAM2) + assert.equal(await p.textContent(".msg.is-live .msg__status"), "Running bake --slow") + // The server's status frame (event_status.json), its clock set 65 s back from now. + const status = fixture("event_status") + await p.evaluate(([path, f]) => { + window.__mock.frame(path, "status", { ...f, progress: { ...f.progress, since: Date.now() - 65000 } }) + }, [STREAM2, status]) const line = () => p.textContent(".msg.is-live .msg__status") assert.equal(await line(), "Running bash: bake --slow · 1m 05s") assert.equal(await p.getAttribute(".msg.is-live .msg__status", "data-phase"), "running_tool") @@ -646,59 +654,171 @@ test("B1: a reply being written draws the stream's progress, its clock moving, a // A data-only frame with progress (the ask frame carries it too): the line follows. await p.evaluate((path) => window.__mock.frame(path, "ask", { cards: [], progress: { phase: "waiting_for_person", since: Date.now(), what: "approval", tool: "bash" } }), STREAM2) assert.equal(await line(), "Waiting for you: allow bash? · 0s") - // Nothing running: `progress: null`, and the status text is drawn again. - await p.evaluate((path) => window.__mock.frame(path, "status", { text: "Writing…", progress: null }), STREAM2) + // Nothing running (event_status_idle.json: no text, `progress: null`): the plain words again. + await p.evaluate(([path, f]) => window.__mock.frame(path, "status", f), [STREAM2, fixture("event_status_idle")]) assert.equal(await line(), "Writing…") - // An older server sends no progress at all: its status text, as before. - await p.evaluate((path) => window.__mock.frame(path, "status", { text: "Running bake --slow" }), STREAM2) - assert.equal(await line(), "Running bake --slow") + // Waiting for a person's answer (event_status_person.json): the phase, not the text. + await p.evaluate(([path, f]) => window.__mock.frame(path, "status", { ...f, progress: { ...f.progress, since: Date.now() } }), [STREAM2, fixture("event_status_person")]) + assert.equal(await line(), "Waiting for you: a question · 0s") // Stopped and failed have no clock. - await p.evaluate((path) => window.__mock.frame(path, "status", { text: "Stopped.", progress: { phase: "stopped", since: Date.now() - 4000, reason: "person" } }), STREAM2) + await p.evaluate(([path, f]) => window.__mock.frame(path, "status", f), [STREAM2, fixture("event_status_stopped")]) assert.equal(await line(), "Stopped: by you") assert.deepEqual(p.errors, []) }) -// --- C4 (ahead of the server): the subagent tree as spec 3.4.0 §12 gives it --------------------- +// --- C4: subagents (spec 3.4.0) --------------------------------------------------------------- // -// ui/js/tree.js draws subagents.md §12's data; the activity panel takes it up once the server's -// /api/v2 carries the tree. Until then it is checked here on the spec's own example. +// The server's /api/v2 (its fixtures, copied into tests/ui/fixtures): GET /chats/{chat}/tree -> +// {tree, room, live}, the stream's `tree` frame, a subagent's transcript and findings, POST +// .../stop; an agent chat's device away -> the kept tree (`live` false) and 409 for the rest; +// /models?kind=agent&device= for a new agent chat's Subagents; `subagent` on an approval item. -const TREE = { - min: 2, max: 4, size: 3, open_slots: 1, - agents: [ - { id: "lead", name: "lead", position: "the main agent", parent: null, state: "working", model: "qwen-27b", mode: "edit", request: [], manager: true, context: { used: 41200, window: 131072 }, progress: { phase: "thinking", since: 0 } }, - { id: "s1", name: "parser", position: "reviews the parser", parent: "lead", state: "working", model: "qwen-27b", mode: "manual", request: ["write"], manager: true, task: "Read src/parse.ts", context: { used: 9100, window: 131072 }, progress: { phase: "running_tool", since: 0, tool: "read", what: "src/parse.ts" }, findings: 2, messages: 3 }, - { id: "s3", name: "lexer", position: "the lexer's tests", parent: "s1", state: "waiting", model: "qwen-27b", mode: "manual", request: [], manager: false, context: { used: 0, window: 131072 }, progress: null, findings: 0, messages: 0 }, - ], - ended: [{ id: "s0", name: "scout", position: "maps the repository", parent: "lead", state: "out_of_context", ended: 0, findings: 1 }], +const ACTIVITY = { activity: { version: "v1", summary: { active: 0 }, agents: [], context: null, outputs: [], team: null, plan: null } } + +function c4(over = {}) { + return b3({ + "GET /chats/chat2/activity": ACTIVITY, + "GET /chats/chat2/tree": fixture("tree"), + "GET /chats/chat2/subagents/s2/transcript": fixture("subagent_transcript"), + "GET /chats/chat2/subagents/s2/findings": fixture("subagent_findings"), + "POST /chats/chat2/subagents/s2/stop": { stopped: ["s2"] }, + ...over, + }) } -test("C4: the tree in order, each agent's position, state, mode, context and phase; slots; the ended ones apart", async () => { - const p = await page(b3()) - const got = await p.evaluate(async (tree) => { - const m = await import("/js/tree.js") - const calls = [] - const box = m.treePart(tree, { room: [{ n: 1, from: "lead", to: ["parser"], text: "Start with the grammar.\nThen the lexer." }], now: 65000, act: { open: (a) => calls.push(["open", a.id]), findings: (a) => calls.push(["findings", a.id]), stop: (a) => calls.push(["stop", a.id]) } }) - document.body.append(box) - const rows = [...box.querySelectorAll(".subagents__list > .subagent:not(.subagent--slot)")].map((r) => ({ id: r.dataset.id, depth: r.style.getPropertyValue("--depth"), text: r.textContent })) - box.querySelector('[data-id="s1"] .subagent__actions .btn').click() - box.querySelector('[data-id="s0"] .subagent__actions .btn:last-child').click() - box.querySelector('[data-id="s1"] .agent__stop').click() - return { rows, slots: box.querySelectorAll(".subagent--slot").length, endedStop: box.querySelectorAll('[data-id="s0"] button.agent__stop').length, calls } - }, TREE) - assert.deepEqual(got.rows.map((r) => [r.id, r.depth]), [["lead", "0"], ["s1", "1"], ["s3", "2"], ["s0", "0"]]) - const parser = got.rows[1].text - assert.match(parser, /parser \(s1\) — reviews the parser/) - assert.match(parser, /6%/, "context: floor(9100 × 100 / 131072)") - assert.match(parser, /manual \+ write/) - assert.match(parser, /Running read: src\/parse\.ts · 1m 05s/) - assert.match(parser, /Messages: 3/) - assert.match(parser, /lead: Start with the grammar\./) - assert.match(got.rows[2].text, /waiting for a slot/) - assert.match(got.rows[3].text, /out of context/) - assert.equal(got.slots, 1) - assert.equal(got.endedStop, 0, "an ended subagent cannot be stopped") - assert.deepEqual(got.calls, [["open", "s1"], ["findings", "s0"], ["stop", "s1"]]) +async function openTree(p) { + await openChatRow(p, "chat2") + await p.click("#activity-open") + await p.waitForSelector("#activity .subagents .subagent") +} + +const rows = (p) => p.evaluate(() => [...document.querySelectorAll("#activity .subagents__list > .subagent")].map((r) => [r.dataset.id, r.style.getPropertyValue("--depth")])) + +test("C4: the activity panel draws the server's tree: nested by parent, slots, the ended ones apart", async () => { + const p = await page(c4()) + await openTree(p) + assert.deepEqual(await rows(p), [["lead", "0"], ["s1", "1"], ["s2", "2"]]) + const routes = await p.textContent('#activity .subagent[data-id="s2"]') + assert.match(routes, /routes/) + assert.match(routes, /writes the routes/) + assert.match(routes, /Running read: src\/api\.py/) + assert.match(routes, /6%/, "context: floor(9100 × 100 / 131072)") + assert.match(routes, /edit \+ execute/) + assert.match(routes, /messages: 1/) + assert.match(routes, /routes → area: Routes are in\./, "the last room message to or from it") + assert.match(await p.textContent('#activity .subagent[data-id="lead"]'), /the main agent/) + assert.match(await p.textContent("#activity .subagents"), /3 of 4/) + assert.match(await p.textContent("#activity .subagents"), /They run on the computer this chat runs on\./) + // The ended one, folded apart, with how it ended, and no stop square. + await p.click("#activity .subagents__ended > summary") + const scout = await p.textContent('#activity .subagents__ended .subagent[data-id="s3"]') + assert.match(scout, /done/) + assert.equal(await p.locator('#activity .subagent[data-id="s3"] button.agent__stop').count(), 0) + assert.equal(await p.locator('#activity .subagent[data-id="lead"] button.agent__stop').count(), 0, "the main agent is not stopped from here") + if (process.env.SHOTS) await p.screenshot({ path: join(process.env.SHOTS, "c4-tree-1280.png") }) + assert.deepEqual(p.errors, []) +}) + +test("C4: a subagent's conversation and findings open read-only; Stop asks, then stops it", async () => { + const p = await page(c4()) + await openTree(p) + await p.click('#activity .subagent[data-id="s2"] > summary') + await p.click('#activity .subagent[data-id="s2"] .subagent__buttons .btn >> text=Open its conversation') + await p.waitForSelector("#activity .convo .convo__turn") + const convo = await p.textContent("#activity-body") + assert.match(convo, /Write the routes\./) + assert.match(convo, /Reading the API first\./) + assert.match(convo, /src\/api\.py/) + assert.equal(await p.locator("#activity-body textarea, #activity-body input").count(), 0, "nothing to type into") + await p.click("#activity-back") + await p.click('#activity .subagent[data-id="s2"] .subagent__buttons .btn >> text=Findings') + await p.waitForSelector("#activity .findings") + assert.match(await p.textContent("#activity .findings"), /Login is POST \/session\./) + await p.click("#activity-back") + await p.click('#activity .subagent[data-id="s2"] button.agent__stop') + await p.waitForSelector("dialog[open]") + assert.match(await p.textContent("dialog[open]"), /Stop routes and the subagents under it\?/) + await p.click("dialog[open] .btn--danger") + await p.waitForFunction(() => window.__mock.api("POST", "/chats/chat2/subagents/s2/stop").length === 1) + assert.deepEqual(p.errors, []) +}) + +test("C4: the stream's tree frame redraws the panel; with the device away the kept tree shows and the rest says why", async () => { + const away = { ...fixture("tree"), live: false } + const p = await page(c4({ "GET /chats/chat2/tree": away, "GET /chats/chat2/subagents/s2/transcript": { __error: { status: 409, detail: "That is kept on the computer this chat runs on, which is not linked right now." } } })) + await openTree(p) + assert.match(await p.textContent("#activity .subagents"), /This device is not linked right now\./) + await p.click('#activity .subagent[data-id="s2"] > summary') + await p.click('#activity .subagent[data-id="s2"] .subagent__buttons .btn >> text=Open its conversation') + await p.waitForSelector("#activity-body .is-error") + assert.match(await p.textContent("#activity-body"), /not linked right now/) + await p.click("#activity-back") + // A reply streams; its `tree` frame (event_tree.json) is the tree now. + await p.evaluate(() => window.__mock.frame("/chats/chat2/messages/m2/stream", "tree", null)) + await p.context().close() + const live = await page({ ...c4(), routes: { ...c4().routes, "GET /chats/chat2": { chat: agentChat(), messages: [userMessage("m1", "Go")], more: false, live: "m2" } }, streams: { [STREAM2]: [] } }) + await openTree(live) + await live.evaluate(([path, f]) => window.__mock.frame(path, "tree", f), [STREAM2, fixture("event_tree")]) + await live.waitForFunction(() => !!document.querySelector('#activity .subagent[data-id="s2"]') && /parser/.test(document.querySelector('#activity .subagent[data-id="s2"]').textContent)) + assert.match(await live.textContent('#activity .subagent[data-id="s2"]'), /reviews the parser/) + assert.match(await live.textContent('#activity .subagent[data-id="s2"]'), /manual \+ write/) + assert.deepEqual(live.errors, []) +}) + +test("C4: an approval a subagent asks for names it", async () => { + const card = fixture("event_ask").cards.find((c) => c.id === "card4") + const p = await page(agentChatOpen([["ask", { cards: [card] }]], { "POST /chats/chat2/interactions/card4": { answered: true } })) + await openChatRow(p, "chat2") + await p.waitForSelector("#cards .ask__subagent") + assert.equal(await p.textContent("#cards .ask__subagent"), "Subagent parser (s1), reviews the parser, asks:") + assert.equal(await p.inputValue("#cards .ask__edit"), "git push", "the command, correctable, as for the main agent's") + assert.deepEqual(p.errors, []) +}) + +test("C4: Subagents for a new agent chat as /models?kind=agent says for this device; for an open one as the chat says", async () => { + const facts = fixture("models_agent") + const newAgent = async (models) => { + const p = await page(c4({ "GET /models?kind=agent&device=token1": models })) + await p.click("#new-chat-btn") + await p.click(".kind__tab >> text=Agent") + await p.waitForSelector("#composer-chips .cchip") + await p.waitForTimeout(200) + return p + } + let p = await newAgent(facts) + assert.equal(await p.locator("#composer-chips .cchip--subagents").count(), 1, "offered, and the device runs them") + await p.click("#composer-chips .cchip--subagents") + await p.waitForSelector(".range2") + assert.equal(await p.getAttribute(".range2__input--min", "max"), "32", "the server's cap") + await p.context().close() + p = await newAgent({ ...facts, agent_subagents: { offered: true, device_runs: false } }) + assert.equal(await p.locator("#composer-chips .cchip--subagents").count(), 0, "this computer's CLI cannot run them") + await p.context().close() + p = await newAgent({ ...facts, agent_subagents: { offered: false, device_runs: true } }) + assert.equal(await p.locator("#composer-chips .cchip--subagents").count(), 0, "not offered") + await p.context().close() + p = await newAgent({ __error: { status: 400, detail: "older server" } }) + assert.equal(await p.locator("#composer-chips .cchip--subagents").count(), 0, "a server before 3.4.0: none, as before") + await p.context().close() + // An open agent chat: its own `subagents` (chat_agent.json offers them). + const chat = fixture("chat_agent") + p = await page(c4({ "GET /chats/chat2": { chat: { ...agentChat({ running: false }), subagents: chat.subagents, agents: chat.agents }, messages: [userMessage("m1", "Go")], more: false, live: null } })) + await openChatRow(p, "chat2") + await p.waitForSelector("#composer-chips .cchip--subagents") + assert.deepEqual(p.errors, []) +}) + +test("C4: the tree at 390 px stays inside the window", async () => { + const p = await page(c4(), { width: 390, height: 844 }) + await p.evaluate(() => document.querySelector('.chatrow[data-id="chat2"]').click()) + await p.waitForSelector("#activity-open") + await p.click("#activity-open") + await p.waitForSelector("#activity .subagents .subagent") + await p.click('#activity .subagent[data-id="s2"] > summary') + assert.ok(await p.evaluate(() => document.documentElement.scrollWidth <= 390), "nothing scrolls sideways") + const panel = await p.evaluate(() => { const b = document.querySelector("#activity-body"); return b.scrollWidth <= b.clientWidth }) + assert.ok(panel, "the panel does not scroll sideways") + if (process.env.SHOTS) await p.screenshot({ path: join(process.env.SHOTS, "c4-tree-390.png") }) assert.deepEqual(p.errors, []) }) diff --git a/ui/app.css b/ui/app.css index 8a32db3..3e4bdc5 100644 --- a/ui/app.css +++ b/ui/app.css @@ -1614,6 +1614,21 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); } } .termpanel__last:empty { display: none; } +/* --- The subagent tree (spec 3.4.0) ------------------------------------------------------------ + Rows are the tasks' rows (.agent): name and id on one line, the position, then the phase (or how + it ended) clipped to one line -- its clock is in the opened row. Ended ones are dimmed. */ + +.subagents { display: grid; gap: var(--sp-2); min-width: 0; } +.subagents .tasks__team, .subagents .tasks__goal { margin: 0; } +.subagent__who { display: flex; gap: var(--sp-2); align-items: baseline; min-width: 0; } +.subagent__who .agent__id { flex: none; } +.subagent__phase { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: var(--text-xs); color: var(--ink-muted); } +.agent.is-ended { opacity: var(--opacity-dim); } +.subagent__buttons { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: 0; } +.findings { margin: 0; padding: var(--sp-3) var(--sp-3) var(--sp-3) calc(var(--sp-3) + var(--sp-5)); display: grid; gap: var(--sp-3); } +.findings__item { min-width: 0; overflow-wrap: anywhere; } +.ask__subagent { color: var(--ink); } + /* --- Narrower windows ---------------------------------------------------------------------- */ /* 64rem (--bp-wide): a panel goes over the chat instead of beside it. */ diff --git a/ui/i18n.js b/ui/i18n.js index 692f861..9b45e71 100644 --- a/ui/i18n.js +++ b/ui/i18n.js @@ -23,6 +23,7 @@ const LEMBAS_I18N = { "%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.", "%(percent)s%% of the context window used": "Využito: %(percent)s %% kontextového okna", "%(percent)s%% used": "Využito: %(percent)s %%", + "%(size)s of %(max)s": "%(size)s z %(max)s", "%(steps)s steps, %(tools)s tools": "Kroky: %(steps)s, nástroje: %(tools)s", "%(tokens)s tokens": "%(tokens)s tokenů", "%(tokens)s until auto-compact": "%(tokens)s do automatického shrnutí", @@ -73,6 +74,7 @@ const LEMBAS_I18N = { "Command": "Příkaz", "Commands": "Příkazy", "Compact": "Shrnout", + "Compacting the conversation": "Shrnuje se konverzace", "Compacting…": "Shrnuje se…", "Compaction buffer": "Rezerva na zhuštění", "Compacts before the next reply.": "Shrne se před další odpovědí.", @@ -83,6 +85,7 @@ const LEMBAS_I18N = { "Connecting…": "Připojuje se…", "Context": "Kontext", "Context and background tasks": "Kontext a úlohy na pozadí", + "Context used": "Využitý kontext", "Continue in a terminal": "Pokračovat v terminálu", "Continue in a terminal…": "Pokračovat v terminálu…", "Copied": "Zkopírováno", @@ -112,12 +115,15 @@ const LEMBAS_I18N = { "Edit message": "Upravit zprávu", "Edit — asks before commands": "Edit — ptá se před příkazy", "Empty folder": "Prázdná složka", + "Ended": "Skončené", "Environment": "Prostředí", "Estimated: no reply has been counted yet.": "Odhad: žádná odpověď ještě nebyla spočítána.", "Every result, even with nothing to report.": "Každý výsledek, i když není co hlásit.", "Every weekday at 8:00, summarise the news on my reading list": "Každý pracovní den v 8:00 shrň zprávy z mého seznamu ke čtení", "Failed": "Selhalo", + "Failed: {{reason}}": "Selhalo: {{reason}}", "Files": "Soubory", + "Findings": "Zjištění", "Finished %(n)s": "Dokončeno: %(n)s", "Folder": "Složka", "Free space": "Volné místo", @@ -138,6 +144,7 @@ const LEMBAS_I18N = { "Link it again to give the agent a new token.": "Propojte ho znovu a agent dostane nový token.", "Link it to your server, and agent chats can work in its folders.": "Propojte ho se svým serverem a agentní konverzace budou moci pracovat v jeho složkách.", "Link this computer": "Propojit tento počítač", + "Loading {{model}}": "Načítá se {{model}}", "Loading…": "Načítá se…", "Looking for a LLeMbas server there…": "Hledám tam server LLeMbas…", "MCP tools": "Nástroje MCP", @@ -189,6 +196,7 @@ const LEMBAS_I18N = { "Open in the web interface": "Otevřít ve webovém rozhraní", "Open its chat": "Otevřít jeho konverzaci", "Open its conversation": "Otevřít jeho konverzaci", + "Open slots: %(n)s": "Volná místa: %(n)s", "Open the activity panel": "Otevřít panel aktivity", "Open the chat": "Otevřít konverzaci", "Open the page again": "Otevřít stránku znovu", @@ -203,7 +211,11 @@ const LEMBAS_I18N = { "Preview": "Náhled", "Project folders": "Složky projektů", "Put the last command and its output into the message box": "Vložit poslední příkaz a jeho výstup do pole zprávy", + "Queued": "Ve frontě", + "Queued: {{ahead}} ahead": "Ve frontě: před ním {{ahead}}", "Read again": "Načíst znovu", + "Reading the prompt": "Čte se výzva", + "Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Čte se výzva: {{done}} / {{total}} tokenů ({{percent}} %)", "Reasoning effort": "Náročnost uvažování", "Recommended": "Doporučeno", "Reconnecting…": "Znovu se připojuje…", @@ -217,11 +229,14 @@ const LEMBAS_I18N = { "Result": "Výsledek", "Resume": "Pokračovat", "Resume %(title)s": "Obnovit %(title)s", + "Retrying in {{in}} s: {{reason}}": "Nový pokus za {{in}} s: {{reason}}", "Room": "Místnost", "Run now": "Spustit teď", "Run this in a terminal on the computer this chat works on: the agent's session goes on there, and the chat keeps it.": "Spusťte to v terminálu na počítači, na kterém tato konverzace pracuje: relace agenta tam pokračuje a konverzace si ji ponechá.", "Run this in a terminal: the agent's session goes on there, in this chat's folder, and the chat keeps it.": "Spusťte to v terminálu: relace agenta tam pokračuje, ve složce této konverzace, a konverzace si ji ponechá.", "Running": "Běží", + "Running {{tool}}": "Spouští {{tool}}", + "Running {{tool}}: {{what}}": "Spouští {{tool}}: {{what}}", "Runs": "Spuštění", "Save": "Uložit", "Saving starts the reply again; everything after this message is dropped.": "Uložením se odpověď spustí znovu; vše po této zprávě se zahodí.", @@ -252,10 +267,13 @@ const LEMBAS_I18N = { "Stop": "Zastavit", "Stop %(name)s": "Zastavit %(name)s", "Stop %(name)s and the members under it? They leave the team.": "Zastavit %(name)s a členy pod ním? Opustí tým.", + "Stop %(name)s and the subagents under it?": "Zastavit %(name)s a podagenty pod ním?", "Stop the agent": "Zastavit agenta", "Stopped": "Zastaveno", "Stopped.": "Zastaveno.", + "Stopped: {{reason}}": "Zastaveno: {{reason}}", "Subagent": "Podagent", + "Subagent %(name)s (%(id)s), %(position)s, asks:": "Podagent %(name)s (%(id)s), %(position)s, se ptá:", "Subagents": "Podagenti", "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í.", "System prompt": "Systémový pokyn", @@ -297,10 +315,12 @@ const LEMBAS_I18N = { "The terminal could not be loaded.": "Terminál se nepodařilo načíst.", "The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Terminál potřebuje novější server LLeMbas. Požádejte jeho správce o aktualizaci.", "These words give no run in the future.": "Z těchto slov nevychází žádné budoucí spuštění.", + "They run on the computer this chat runs on.": "Běží na počítači, na kterém běží tato konverzace.", "Think before answering": "Přemýšlet před odpovědí", "Thinking": "Přemýšlí", "This computer": "Tento počítač", "This computer is not linked yet.": "Tento počítač ještě není propojený.", + "This device is not linked right now.": "Toto zařízení teď není připojené.", "This file is too big to show here (%(size)s).": "Tento soubor je příliš velký na zobrazení (%(size)s).", "This file is too large to send.": "Tento soubor je příliš velký na odeslání.", "This is a folder; attach the files in it.": "Toto je složka; přiložte soubory v ní.", @@ -320,18 +340,24 @@ const LEMBAS_I18N = { "Up to %(n)s at once": "Nejvýše %(n)s současně", "Wait until the files are sent.": "Počkejte, až se soubory odešlou.", "Waiting for a free model": "Čeká na volný model", + "Waiting for subagents: {{running}} running": "Čeká na podagenty: pracuje {{running}}", "Waiting for the browser…": "Čeká se na prohlížeč…", "Waiting for the reply before it": "Čeká na odpověď před ní", + "Waiting for you: {{what}}": "Čeká na vás: {{what}}", "Waiting for your answer": "Čeká na vaši odpověď", + "Waiting for {{model}}: {{reason}}": "Čeká se na {{model}}: {{reason}}", "What it does": "Co dělá", "What should happen, and when": "Co se má stát a kdy", "When": "Kdy", "When it matters": "Když na tom záleží", + "Writing": "Píše", + "Writing: {{rate}} tok/s": "Píše: {{rate}} tok/s", "Writing…": "Píše se…", "Written to %(path)s": "Zapsáno do %(path)s", "You asked: “%(text)s”": "Požádali jste: „%(text)s“", "Your answer": "Vaše odpověď", "Your files on the server, as the chats see them.": "Vaše soubory na serveru, tak jak je vidí konverzace.", + "a question": "otázka", "agent connecting…": "agent se připojuje…", "agent off": "agent vypnutý", "agent refused": "agent odmítnut", @@ -341,24 +367,41 @@ const LEMBAS_I18N = { "agent stopped": "agent zastaven", "agent waiting": "agent čeká", "agent: no folders": "agent: žádné složky", + "all its slots are in use": "všechny jeho sloty jsou obsazené", + "allow {{tool}}?": "povolit {{tool}}?", + "an approval": "povolení", + "budget": "rozpočet", "busy": "zaneprázdněný", + "by you": "vámi", "caught up": "dohnáno", "default": "výchozí", "done": "hotovo", "failed": "selhalo", + "findings: %(n)s": "zjištění: %(n)s", + "finished": "dokončený", "free": "volný", + "idle": "nečinný", + "it serves one request at a time and is answering another": "vyřizuje jeden požadavek najednou a právě vyřizuje jiný", + "its server holds another model now": "jeho server má teď načtený jiný model", "late": "se zpožděním", + "messages: %(n)s": "zprávy: %(n)s", "not linked": "nepropojeno", "off": "vypnuto", + "out of context": "bez kontextu", + "paused: context full": "pozastavený: plný kontext", "queued": "ve frontě", "report": "hlášení", "run by hand": "spuštěno ručně", + "step limit": "limit kroků", "stopped": "zastaveno", "switched": "přepnuto", + "the main agent": "hlavní agent", + "the plan": "plán", "tried again %(when)s": "znovu %(when)s", "update": "aktualizace", "update %(version)s": "aktualizace %(version)s", "waiting": "čeká", + "waiting for a slot": "čeká na volný slot", "window unknown": "okno neznámé", "working": "pracuje", "“%(title)s” is deleted on the server, with its messages and files. This cannot be undone.": "„%(title)s“ se smaže na serveru i se svými zprávami a soubory. Nelze to vrátit.", @@ -377,6 +420,7 @@ const LEMBAS_I18N = { "%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.", "%(percent)s%% of the context window used": "%(percent)s %% des Kontextfensters belegt", "%(percent)s%% used": "%(percent)s %% belegt", + "%(size)s of %(max)s": "%(size)s von %(max)s", "%(steps)s steps, %(tools)s tools": "%(steps)s Schritte, %(tools)s Werkzeuge", "%(tokens)s tokens": "%(tokens)s Tokens", "%(tokens)s until auto-compact": "%(tokens)s bis zur automatischen Zusammenfassung", @@ -427,6 +471,7 @@ const LEMBAS_I18N = { "Command": "Befehl", "Commands": "Befehle", "Compact": "Zusammenfassen", + "Compacting the conversation": "Verdichte das Gespräch", "Compacting…": "Wird zusammengefasst…", "Compaction buffer": "Puffer für die Verdichtung", "Compacts before the next reply.": "Wird vor der nächsten Antwort zusammengefasst.", @@ -437,6 +482,7 @@ const LEMBAS_I18N = { "Connecting…": "Verbinde…", "Context": "Kontext", "Context and background tasks": "Kontext und Hintergrundaufgaben", + "Context used": "Genutzter Kontext", "Continue in a terminal": "Im Terminal fortsetzen", "Continue in a terminal…": "Im Terminal fortsetzen…", "Copied": "Kopiert", @@ -466,12 +512,15 @@ const LEMBAS_I18N = { "Edit message": "Nachricht bearbeiten", "Edit — asks before commands": "Edit — fragt vor Befehlen", "Empty folder": "Leerer Ordner", + "Ended": "Beendet", "Environment": "Umgebung", "Estimated: no reply has been counted yet.": "Geschätzt: Noch keine Antwort wurde gezählt.", "Every result, even with nothing to report.": "Jedes Ergebnis, auch ohne Neuigkeiten.", "Every weekday at 8:00, summarise the news on my reading list": "Jeden Werktag um 8:00 die Nachrichten auf meiner Leseliste zusammenfassen", "Failed": "Fehlgeschlagen", + "Failed: {{reason}}": "Fehlgeschlagen: {{reason}}", "Files": "Dateien", + "Findings": "Erkenntnisse", "Finished %(n)s": "Fertig: %(n)s", "Folder": "Ordner", "Free space": "Freier Platz", @@ -492,6 +541,7 @@ const LEMBAS_I18N = { "Link it again to give the agent a new token.": "Verknüpfen Sie ihn erneut, damit der Agent ein neues Token bekommt.", "Link it to your server, and agent chats can work in its folders.": "Verknüpfen Sie ihn mit Ihrem Server, dann können Agent-Chats in seinen Ordnern arbeiten.", "Link this computer": "Diesen Computer verbinden", + "Loading {{model}}": "Lade {{model}}", "Loading…": "Wird geladen…", "Looking for a LLeMbas server there…": "Suche dort nach einem LLeMbas-Server…", "MCP tools": "MCP-Werkzeuge", @@ -543,6 +593,7 @@ const LEMBAS_I18N = { "Open in the web interface": "In der Weboberfläche öffnen", "Open its chat": "Seinen Chat öffnen", "Open its conversation": "Seine Unterhaltung öffnen", + "Open slots: %(n)s": "Freie Plätze: %(n)s", "Open the activity panel": "Aktivitätsbereich öffnen", "Open the chat": "Chat öffnen", "Open the page again": "Seite erneut öffnen", @@ -557,7 +608,11 @@ const LEMBAS_I18N = { "Preview": "Vorschau", "Project folders": "Projektordner", "Put the last command and its output into the message box": "Den letzten Befehl und seine Ausgabe ins Nachrichtenfeld legen", + "Queued": "In der Warteschlange", + "Queued: {{ahead}} ahead": "In der Warteschlange: {{ahead}} davor", "Read again": "Neu lesen", + "Reading the prompt": "Lese den Prompt", + "Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Lese den Prompt: {{done}} / {{total}} Tokens ({{percent}} %)", "Reasoning effort": "Denkaufwand", "Recommended": "Empfohlen", "Reconnecting…": "Verbinde neu…", @@ -571,11 +626,14 @@ const LEMBAS_I18N = { "Result": "Ergebnis", "Resume": "Fortsetzen", "Resume %(title)s": "%(title)s fortsetzen", + "Retrying in {{in}} s: {{reason}}": "Neuer Versuch in {{in}} s: {{reason}}", "Room": "Raum", "Run now": "Jetzt ausführen", "Run this in a terminal on the computer this chat works on: the agent's session goes on there, and the chat keeps it.": "Führen Sie dies in einem Terminal auf dem Computer aus, auf dem dieser Chat arbeitet: Die Sitzung des Agenten geht dort weiter, und der Chat behält sie.", "Run this in a terminal: the agent's session goes on there, in this chat's folder, and the chat keeps it.": "Führen Sie dies in einem Terminal aus: Die Sitzung des Agenten geht dort weiter, im Ordner dieses Chats, und der Chat behält sie.", "Running": "Läuft", + "Running {{tool}}": "Führe {{tool}} aus", + "Running {{tool}}: {{what}}": "Führe {{tool}} aus: {{what}}", "Runs": "Läufe", "Save": "Speichern", "Saving starts the reply again; everything after this message is dropped.": "Speichern startet die Antwort neu; alles nach dieser Nachricht wird verworfen.", @@ -606,10 +664,13 @@ const LEMBAS_I18N = { "Stop": "Stoppen", "Stop %(name)s": "%(name)s stoppen", "Stop %(name)s and the members under it? They leave the team.": "%(name)s und die Mitglieder darunter stoppen? Sie verlassen das Team.", + "Stop %(name)s and the subagents under it?": "%(name)s und die Subagenten darunter stoppen?", "Stop the agent": "Agent anhalten", "Stopped": "Angehalten", "Stopped.": "Angehalten.", + "Stopped: {{reason}}": "Gestoppt: {{reason}}", "Subagent": "Subagent", + "Subagent %(name)s (%(id)s), %(position)s, asks:": "Subagent %(name)s (%(id)s), %(position)s, fragt:", "Subagents": "Subagenten", "Summarise the earlier turns now and send the summary in their place.": "Die früheren Runden jetzt zusammenfassen und stattdessen die Zusammenfassung senden.", "System prompt": "Systemprompt", @@ -651,10 +712,12 @@ const LEMBAS_I18N = { "The terminal could not be loaded.": "Das Terminal konnte nicht geladen werden.", "The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Das Terminal braucht einen neueren LLeMbas-Server. Bitten Sie seinen Administrator, ihn zu aktualisieren.", "These words give no run in the future.": "Diese Worte ergeben keinen künftigen Lauf.", + "They run on the computer this chat runs on.": "Sie laufen auf dem Computer, auf dem dieser Chat läuft.", "Think before answering": "Vor der Antwort nachdenken", "Thinking": "Denkt nach", "This computer": "Dieser Computer", "This computer is not linked yet.": "Dieser Computer ist noch nicht verknüpft.", + "This device is not linked right now.": "Dieses Gerät ist gerade nicht verbunden.", "This file is too big to show here (%(size)s).": "Diese Datei ist zu groß für die Anzeige (%(size)s).", "This file is too large to send.": "Diese Datei ist zu groß zum Senden.", "This is a folder; attach the files in it.": "Das ist ein Ordner; hängen Sie die Dateien darin an.", @@ -674,18 +737,24 @@ const LEMBAS_I18N = { "Up to %(n)s at once": "Bis zu %(n)s gleichzeitig", "Wait until the files are sent.": "Warten Sie, bis die Dateien gesendet sind.", "Waiting for a free model": "Wartet auf ein freies Modell", + "Waiting for subagents: {{running}} running": "Wartet auf Subagenten: {{running}} laufen", "Waiting for the browser…": "Warte auf den Browser…", "Waiting for the reply before it": "Wartet auf die Antwort davor", + "Waiting for you: {{what}}": "Wartet auf dich: {{what}}", "Waiting for your answer": "Wartet auf Ihre Antwort", + "Waiting for {{model}}: {{reason}}": "Warte auf {{model}}: {{reason}}", "What it does": "Was sie tut", "What should happen, and when": "Was soll wann geschehen?", "When": "Wann", "When it matters": "Wenn es zählt", + "Writing": "Schreibe", + "Writing: {{rate}} tok/s": "Schreibe: {{rate}} Tok/s", "Writing…": "Schreibt…", "Written to %(path)s": "Geschrieben nach %(path)s", "You asked: “%(text)s”": "Ihre Worte: „%(text)s“", "Your answer": "Ihre Antwort", "Your files on the server, as the chats see them.": "Ihre Dateien auf dem Server, so wie die Chats sie sehen.", + "a question": "eine Frage", "agent connecting…": "Agent verbindet…", "agent off": "Agent aus", "agent refused": "Agent abgewiesen", @@ -695,24 +764,41 @@ const LEMBAS_I18N = { "agent stopped": "Agent angehalten", "agent waiting": "Agent wartet", "agent: no folders": "Agent: keine Ordner", + "all its slots are in use": "alle seine Plätze sind belegt", + "allow {{tool}}?": "{{tool}} erlauben?", + "an approval": "eine Freigabe", + "budget": "Budget", "busy": "belegt", + "by you": "von dir", "caught up": "nachgeholt", "default": "Standard", "done": "fertig", "failed": "fehlgeschlagen", + "findings: %(n)s": "Erkenntnisse: %(n)s", + "finished": "fertig", "free": "frei", + "idle": "untätig", + "it serves one request at a time and is answering another": "es bedient eine Anfrage auf einmal und beantwortet gerade eine andere", + "its server holds another model now": "sein Server hält gerade ein anderes Modell", "late": "verspätet", + "messages: %(n)s": "Nachrichten: %(n)s", "not linked": "nicht verbunden", "off": "aus", + "out of context": "Kontext erschöpft", + "paused: context full": "pausiert: Kontext voll", "queued": "in der Warteschlange", "report": "Bericht", "run by hand": "von Hand gestartet", + "step limit": "Schrittlimit", "stopped": "angehalten", "switched": "gewechselt", + "the main agent": "der Hauptagent", + "the plan": "den Plan", "tried again %(when)s": "erneut %(when)s", "update": "Update", "update %(version)s": "Update %(version)s", "waiting": "wartet", + "waiting for a slot": "wartet auf einen Platz", "window unknown": "Fenster unbekannt", "working": "arbeitet", "“%(title)s” is deleted on the server, with its messages and files. This cannot be undone.": "„%(title)s“ wird auf dem Server gelöscht, mit seinen Nachrichten und Dateien. Das lässt sich nicht rückgängig machen.", @@ -731,6 +817,7 @@ const LEMBAS_I18N = { "%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.", "%(percent)s%% of the context window used": "%(percent)s %% de la ventana de contexto usado", "%(percent)s%% used": "%(percent)s %% usado", + "%(size)s of %(max)s": "%(size)s de %(max)s", "%(steps)s steps, %(tools)s tools": "%(steps)s pasos, %(tools)s herramientas", "%(tokens)s tokens": "%(tokens)s tokens", "%(tokens)s until auto-compact": "%(tokens)s hasta la compactación automática", @@ -781,6 +868,7 @@ const LEMBAS_I18N = { "Command": "Comando", "Commands": "Comandos", "Compact": "Compactar", + "Compacting the conversation": "Compactando la conversación", "Compacting…": "Compactando…", "Compaction buffer": "Reserva para la compactación", "Compacts before the next reply.": "Se compacta antes de la próxima respuesta.", @@ -791,6 +879,7 @@ const LEMBAS_I18N = { "Connecting…": "Conectando…", "Context": "Contexto", "Context and background tasks": "Contexto y tareas en segundo plano", + "Context used": "Contexto usado", "Continue in a terminal": "Continuar en un terminal", "Continue in a terminal…": "Continuar en un terminal…", "Copied": "Copiado", @@ -820,12 +909,15 @@ const LEMBAS_I18N = { "Edit message": "Editar mensaje", "Edit — asks before commands": "Edit — pregunta antes de los comandos", "Empty folder": "Carpeta vacía", + "Ended": "Terminados", "Environment": "Entorno", "Estimated: no reply has been counted yet.": "Estimado: aún no se ha contado ninguna respuesta.", "Every result, even with nothing to report.": "Cada resultado, incluso sin nada que informar.", "Every weekday at 8:00, summarise the news on my reading list": "Cada día laborable a las 8:00, resume las noticias de mi lista de lectura", "Failed": "Fallidos", + "Failed: {{reason}}": "Error: {{reason}}", "Files": "Archivos", + "Findings": "Hallazgos", "Finished %(n)s": "Terminadas: %(n)s", "Folder": "Carpeta", "Free space": "Espacio libre", @@ -846,6 +938,7 @@ const LEMBAS_I18N = { "Link it again to give the agent a new token.": "Vincúlelo de nuevo para que el agente reciba un token nuevo.", "Link it to your server, and agent chats can work in its folders.": "Vincúlelo a su servidor y los chats de agente podrán trabajar en sus carpetas.", "Link this computer": "Vincular este ordenador", + "Loading {{model}}": "Cargando {{model}}", "Loading…": "Cargando…", "Looking for a LLeMbas server there…": "Buscando un servidor LLeMbas allí…", "MCP tools": "Herramientas MCP", @@ -897,6 +990,7 @@ const LEMBAS_I18N = { "Open in the web interface": "Abrir en la interfaz web", "Open its chat": "Abrir su chat", "Open its conversation": "Abrir su conversación", + "Open slots: %(n)s": "Plazas libres: %(n)s", "Open the activity panel": "Abrir el panel de actividad", "Open the chat": "Abrir el chat", "Open the page again": "Abrir la página de nuevo", @@ -911,7 +1005,11 @@ const LEMBAS_I18N = { "Preview": "Vista previa", "Project folders": "Carpetas de proyectos", "Put the last command and its output into the message box": "Poner el último comando y su salida en el cuadro de mensaje", + "Queued": "En cola", + "Queued: {{ahead}} ahead": "En cola: {{ahead}} por delante", "Read again": "Leer de nuevo", + "Reading the prompt": "Leyendo el prompt", + "Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Leyendo el prompt: {{done}} / {{total}} tokens ({{percent}} %)", "Reasoning effort": "Esfuerzo de razonamiento", "Recommended": "Recomendado", "Reconnecting…": "Reconectando…", @@ -925,11 +1023,14 @@ const LEMBAS_I18N = { "Result": "Resultado", "Resume": "Reanudar", "Resume %(title)s": "Reanudar %(title)s", + "Retrying in {{in}} s: {{reason}}": "Reintento en {{in}} s: {{reason}}", "Room": "Sala", "Run now": "Ejecutar ahora", "Run this in a terminal on the computer this chat works on: the agent's session goes on there, and the chat keeps it.": "Ejecútelo en un terminal del equipo en el que trabaja este chat: la sesión del agente continúa allí y el chat la conserva.", "Run this in a terminal: the agent's session goes on there, in this chat's folder, and the chat keeps it.": "Ejecútelo en un terminal: la sesión del agente continúa allí, en la carpeta de este chat, y el chat la conserva.", "Running": "En ejecución", + "Running {{tool}}": "Ejecutando {{tool}}", + "Running {{tool}}: {{what}}": "Ejecutando {{tool}}: {{what}}", "Runs": "Ejecuciones", "Save": "Guardar", "Saving starts the reply again; everything after this message is dropped.": "Al guardar, la respuesta empieza de nuevo; se descarta todo lo que hay después de este mensaje.", @@ -960,10 +1061,13 @@ const LEMBAS_I18N = { "Stop": "Detener", "Stop %(name)s": "Detener %(name)s", "Stop %(name)s and the members under it? They leave the team.": "¿Detener %(name)s y a los miembros que dependen de él? Dejan el equipo.", + "Stop %(name)s and the subagents under it?": "¿Detener a %(name)s y a los subagentes que tiene debajo?", "Stop the agent": "Detener el agente", "Stopped": "Detenido", "Stopped.": "Detenido.", + "Stopped: {{reason}}": "Detenido: {{reason}}", "Subagent": "Subagente", + "Subagent %(name)s (%(id)s), %(position)s, asks:": "El subagente %(name)s (%(id)s), %(position)s, pregunta:", "Subagents": "Subagentes", "Summarise the earlier turns now and send the summary in their place.": "Resumir ahora los turnos anteriores y enviar el resumen en su lugar.", "System prompt": "Prompt de sistema", @@ -1005,10 +1109,12 @@ const LEMBAS_I18N = { "The terminal could not be loaded.": "No se ha podido cargar el terminal.", "The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "El terminal necesita un servidor LLeMbas más reciente. Pida a su administrador que lo actualice.", "These words give no run in the future.": "Estas palabras no dan ninguna ejecución futura.", + "They run on the computer this chat runs on.": "Se ejecutan en el ordenador en el que se ejecuta este chat.", "Think before answering": "Razonar antes de responder", "Thinking": "Pensando", "This computer": "Este equipo", "This computer is not linked yet.": "Este equipo aún no está vinculado.", + "This device is not linked right now.": "Este dispositivo no está vinculado en este momento.", "This file is too big to show here (%(size)s).": "Este archivo es demasiado grande para mostrarlo (%(size)s).", "This file is too large to send.": "Este archivo es demasiado grande para enviarlo.", "This is a folder; attach the files in it.": "Esto es una carpeta; adjunte los archivos que contiene.", @@ -1028,18 +1134,24 @@ const LEMBAS_I18N = { "Up to %(n)s at once": "Hasta %(n)s a la vez", "Wait until the files are sent.": "Espere a que se envíen los archivos.", "Waiting for a free model": "Esperando un modelo libre", + "Waiting for subagents: {{running}} running": "Esperando a los subagentes: {{running}} en marcha", "Waiting for the browser…": "Esperando al navegador…", "Waiting for the reply before it": "Esperando la respuesta anterior", + "Waiting for you: {{what}}": "Esperándote: {{what}}", "Waiting for your answer": "Esperando su respuesta", + "Waiting for {{model}}: {{reason}}": "Esperando a {{model}}: {{reason}}", "What it does": "Qué hace", "What should happen, and when": "Qué debe ocurrir, y cuándo", "When": "Cuándo", "When it matters": "Cuando importa", + "Writing": "Escribiendo", + "Writing: {{rate}} tok/s": "Escribiendo: {{rate}} tok/s", "Writing…": "Escribiendo…", "Written to %(path)s": "Escrito en %(path)s", "You asked: “%(text)s”": "Usted pidió: «%(text)s»", "Your answer": "Su respuesta", "Your files on the server, as the chats see them.": "Sus archivos en el servidor, tal como los ven los chats.", + "a question": "una pregunta", "agent connecting…": "agente conectando…", "agent off": "agente apagado", "agent refused": "agente rechazado", @@ -1049,24 +1161,41 @@ const LEMBAS_I18N = { "agent stopped": "agente detenido", "agent waiting": "agente en espera", "agent: no folders": "agente: sin carpetas", + "all its slots are in use": "todas sus plazas están ocupadas", + "allow {{tool}}?": "¿permitir {{tool}}?", + "an approval": "una aprobación", + "budget": "presupuesto", "busy": "ocupado", + "by you": "por ti", "caught up": "recuperada", "default": "predeterminado", "done": "terminado", "failed": "falló", + "findings: %(n)s": "hallazgos: %(n)s", + "finished": "terminado", "free": "libre", + "idle": "inactivo", + "it serves one request at a time and is answering another": "atiende una petición a la vez y está respondiendo otra", + "its server holds another model now": "su servidor tiene ahora otro modelo", "late": "con retraso", + "messages: %(n)s": "mensajes: %(n)s", "not linked": "no vinculado", "off": "desactivado", + "out of context": "sin contexto", + "paused: context full": "en pausa: contexto lleno", "queued": "en cola", "report": "informe", "run by hand": "ejecutada a mano", + "step limit": "límite de pasos", "stopped": "detenido", "switched": "cambiado", + "the main agent": "el agente principal", + "the plan": "el plan", "tried again %(when)s": "de nuevo %(when)s", "update": "actualizar", "update %(version)s": "actualizar a %(version)s", "waiting": "esperando", + "waiting for a slot": "esperando una plaza", "window unknown": "ventana desconocida", "working": "trabajando", "“%(title)s” is deleted on the server, with its messages and files. This cannot be undone.": "«%(title)s» se elimina del servidor, con sus mensajes y archivos. No se puede deshacer.", @@ -1085,6 +1214,7 @@ const LEMBAS_I18N = { "%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.", "%(percent)s%% of the context window used": "%(percent)s %% de la fenêtre de contexte utilisés", "%(percent)s%% used": "%(percent)s %% utilisés", + "%(size)s of %(max)s": "%(size)s sur %(max)s", "%(steps)s steps, %(tools)s tools": "%(steps)s étapes, %(tools)s outils", "%(tokens)s tokens": "%(tokens)s jetons", "%(tokens)s until auto-compact": "%(tokens)s avant le compactage automatique", @@ -1135,6 +1265,7 @@ const LEMBAS_I18N = { "Command": "Commande", "Commands": "Commandes", "Compact": "Compacter", + "Compacting the conversation": "Compactage de la conversation", "Compacting…": "Compactage…", "Compaction buffer": "Réserve de compactage", "Compacts before the next reply.": "Compacté avant la prochaine réponse.", @@ -1145,6 +1276,7 @@ const LEMBAS_I18N = { "Connecting…": "Connexion…", "Context": "Contexte", "Context and background tasks": "Contexte et tâches en arrière-plan", + "Context used": "Contexte utilisé", "Continue in a terminal": "Continuer dans un terminal", "Continue in a terminal…": "Continuer dans un terminal…", "Copied": "Copié", @@ -1174,12 +1306,15 @@ const LEMBAS_I18N = { "Edit message": "Modifier le message", "Edit — asks before commands": "Edit — demande avant les commandes", "Empty folder": "Dossier vide", + "Ended": "Terminés", "Environment": "Environnement", "Estimated: no reply has been counted yet.": "Estimation : aucune réponse n’a encore été comptée.", "Every result, even with nothing to report.": "Chaque résultat, même sans rien à signaler.", "Every weekday at 8:00, summarise the news on my reading list": "Chaque jour ouvré à 8 h, résume les nouvelles de ma liste de lecture", "Failed": "Échecs", + "Failed: {{reason}}": "Échec : {{reason}}", "Files": "Fichiers", + "Findings": "Constats", "Finished %(n)s": "Terminées : %(n)s", "Folder": "Dossier", "Free space": "Espace libre", @@ -1200,6 +1335,7 @@ const LEMBAS_I18N = { "Link it again to give the agent a new token.": "Liez-le à nouveau pour donner un nouveau jeton à l’agent.", "Link it to your server, and agent chats can work in its folders.": "Liez-le à votre serveur, et les conversations d’agent pourront travailler dans ses dossiers.", "Link this computer": "Relier cet ordinateur", + "Loading {{model}}": "Chargement de {{model}}", "Loading…": "Chargement…", "Looking for a LLeMbas server there…": "Recherche d'un serveur LLeMbas à cette adresse…", "MCP tools": "Outils MCP", @@ -1251,6 +1387,7 @@ const LEMBAS_I18N = { "Open in the web interface": "Ouvrir dans l'interface web", "Open its chat": "Ouvrir sa conversation", "Open its conversation": "Ouvrir sa conversation", + "Open slots: %(n)s": "Places libres : %(n)s", "Open the activity panel": "Ouvrir le panneau d’activité", "Open the chat": "Ouvrir la conversation", "Open the page again": "Rouvrir la page", @@ -1265,7 +1402,11 @@ const LEMBAS_I18N = { "Preview": "Aperçu", "Project folders": "Dossiers de projets", "Put the last command and its output into the message box": "Mettre la dernière commande et sa sortie dans la zone de message", + "Queued": "En file d’attente", + "Queued: {{ahead}} ahead": "En file d’attente : {{ahead}} avant", "Read again": "Relire", + "Reading the prompt": "Lecture du prompt", + "Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Lecture du prompt : {{done}} / {{total}} jetons ({{percent}} %)", "Reasoning effort": "Effort de raisonnement", "Recommended": "Recommandé", "Reconnecting…": "Reconnexion…", @@ -1279,11 +1420,14 @@ const LEMBAS_I18N = { "Result": "Résultat", "Resume": "Reprendre", "Resume %(title)s": "Reprendre %(title)s", + "Retrying in {{in}} s: {{reason}}": "Nouvel essai dans {{in}} s : {{reason}}", "Room": "Salle", "Run now": "Exécuter maintenant", "Run this in a terminal on the computer this chat works on: the agent's session goes on there, and the chat keeps it.": "Exécutez ceci dans un terminal de l'ordinateur sur lequel travaille cette discussion : la session de l'agent s'y poursuit, et la discussion la conserve.", "Run this in a terminal: the agent's session goes on there, in this chat's folder, and the chat keeps it.": "Exécutez ceci dans un terminal : la session de l'agent s'y poursuit, dans le dossier de cette discussion, et la discussion la conserve.", "Running": "En cours", + "Running {{tool}}": "Exécution de {{tool}}", + "Running {{tool}}: {{what}}": "Exécution de {{tool}} : {{what}}", "Runs": "Exécutions", "Save": "Enregistrer", "Saving starts the reply again; everything after this message is dropped.": "Enregistrer relance la réponse ; tout ce qui suit ce message est supprimé.", @@ -1314,10 +1458,13 @@ const LEMBAS_I18N = { "Stop": "Arrêter", "Stop %(name)s": "Arrêter %(name)s", "Stop %(name)s and the members under it? They leave the team.": "Arrêter %(name)s et les membres qui en dépendent ? Ils quittent l’équipe.", + "Stop %(name)s and the subagents under it?": "Arrêter %(name)s et les sous-agents en dessous ?", "Stop the agent": "Arrêter l'agent", "Stopped": "Arrêté", "Stopped.": "Arrêté.", + "Stopped: {{reason}}": "Arrêté : {{reason}}", "Subagent": "Sous-agent", + "Subagent %(name)s (%(id)s), %(position)s, asks:": "Le sous-agent %(name)s (%(id)s), %(position)s, demande :", "Subagents": "Sous-agents", "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.", "System prompt": "Prompt système", @@ -1359,10 +1506,12 @@ const LEMBAS_I18N = { "The terminal could not be loaded.": "Le terminal n'a pas pu être chargé.", "The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Le terminal nécessite un serveur LLeMbas plus récent. Demandez à son administrateur de le mettre à jour.", "These words give no run in the future.": "Ces mots ne donnent aucune exécution à venir.", + "They run on the computer this chat runs on.": "Ils tournent sur l’ordinateur où tourne cette conversation.", "Think before answering": "Réfléchir avant de répondre", "Thinking": "Réflexion", "This computer": "Cet ordinateur", "This computer is not linked yet.": "Cet ordinateur n’est pas encore lié.", + "This device is not linked right now.": "Cet appareil n'est pas relié pour le moment.", "This file is too big to show here (%(size)s).": "Ce fichier est trop volumineux pour être affiché (%(size)s).", "This file is too large to send.": "Ce fichier est trop volumineux pour être envoyé.", "This is a folder; attach the files in it.": "C'est un dossier ; joignez les fichiers qu'il contient.", @@ -1382,18 +1531,24 @@ const LEMBAS_I18N = { "Up to %(n)s at once": "Jusqu’à %(n)s à la fois", "Wait until the files are sent.": "Attendez que les fichiers soient envoyés.", "Waiting for a free model": "En attente d’un modèle libre", + "Waiting for subagents: {{running}} running": "En attente des sous-agents : {{running}} en cours", "Waiting for the browser…": "En attente du navigateur…", "Waiting for the reply before it": "En attente de la réponse précédente", + "Waiting for you: {{what}}": "En attente de vous : {{what}}", "Waiting for your answer": "En attente de votre réponse", + "Waiting for {{model}}: {{reason}}": "En attente de {{model}} : {{reason}}", "What it does": "Ce qu’elle fait", "What should happen, and when": "Ce qui doit se passer, et quand", "When": "Quand", "When it matters": "Quand c'est important", + "Writing": "Écriture", + "Writing: {{rate}} tok/s": "Écriture : {{rate}} jetons/s", "Writing…": "Rédaction…", "Written to %(path)s": "Écrit dans %(path)s", "You asked: “%(text)s”": "Votre demande : « %(text)s »", "Your answer": "Votre réponse", "Your files on the server, as the chats see them.": "Vos fichiers sur le serveur, tels que les conversations les voient.", + "a question": "une question", "agent connecting…": "agent en connexion…", "agent off": "agent arrêté", "agent refused": "agent refusé", @@ -1403,24 +1558,41 @@ const LEMBAS_I18N = { "agent stopped": "agent interrompu", "agent waiting": "agent en attente", "agent: no folders": "agent : aucun dossier", + "all its slots are in use": "tous ses emplacements sont occupés", + "allow {{tool}}?": "autoriser {{tool}} ?", + "an approval": "une approbation", + "budget": "budget", "busy": "occupé", + "by you": "par vous", "caught up": "rattrapée", "default": "par défaut", "done": "terminé", "failed": "échoué", + "findings: %(n)s": "constats : %(n)s", + "finished": "terminé", "free": "libre", + "idle": "inactif", + "it serves one request at a time and is answering another": "il traite une requête à la fois et en traite une autre", + "its server holds another model now": "son serveur a maintenant un autre modèle chargé", "late": "en retard", + "messages: %(n)s": "messages : %(n)s", "not linked": "non relié", "off": "désactivé", + "out of context": "contexte épuisé", + "paused: context full": "en pause : contexte plein", "queued": "en file d’attente", "report": "rapport", "run by hand": "lancée à la main", + "step limit": "limite d’étapes", "stopped": "arrêté", "switched": "changé", + "the main agent": "l’agent principal", + "the plan": "le plan", "tried again %(when)s": "de nouveau %(when)s", "update": "mise à jour", "update %(version)s": "mise à jour %(version)s", "waiting": "en attente", + "waiting for a slot": "attend une place", "window unknown": "fenêtre inconnue", "working": "au travail", "“%(title)s” is deleted on the server, with its messages and files. This cannot be undone.": "« %(title)s » est supprimée du serveur, avec ses messages et ses fichiers. C'est définitif.", @@ -1439,6 +1611,7 @@ const LEMBAS_I18N = { "%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.", "%(percent)s%% of the context window used": "A kontextusablak %(percent)s%%-a foglalt", "%(percent)s%% used": "%(percent)s%% foglalt", + "%(size)s of %(max)s": "%(size)s / %(max)s", "%(steps)s steps, %(tools)s tools": "%(steps)s lépés, %(tools)s eszköz", "%(tokens)s tokens": "%(tokens)s token", "%(tokens)s until auto-compact": "%(tokens)s az automatikus tömörítésig", @@ -1489,6 +1662,7 @@ const LEMBAS_I18N = { "Command": "Parancs", "Commands": "Parancsok", "Compact": "Tömörítés", + "Compacting the conversation": "A beszélgetés tömörítése", "Compacting…": "Tömörítés…", "Compaction buffer": "Tömörítési tartalék", "Compacts before the next reply.": "A következő válasz előtt tömörül.", @@ -1499,6 +1673,7 @@ const LEMBAS_I18N = { "Connecting…": "Kapcsolódás…", "Context": "Kontextus", "Context and background tasks": "Kontextus és háttérfeladatok", + "Context used": "Felhasznált kontextus", "Continue in a terminal": "Folytatás terminálban", "Continue in a terminal…": "Folytatás terminálban…", "Copied": "Másolva", @@ -1528,12 +1703,15 @@ const LEMBAS_I18N = { "Edit message": "Üzenet szerkesztése", "Edit — asks before commands": "Edit — parancsok előtt kérdez", "Empty folder": "Üres mappa", + "Ended": "Befejezett", "Environment": "Környezet", "Estimated: no reply has been counted yet.": "Becslés: még egy válasz sem lett megszámolva.", "Every result, even with nothing to report.": "Minden eredmény, akkor is, ha nincs mit jelenteni.", "Every weekday at 8:00, summarise the news on my reading list": "Minden hétköznap 8:00-kor foglald össze az olvasólistám híreit", "Failed": "Sikertelen", + "Failed: {{reason}}": "Sikertelen: {{reason}}", "Files": "Fájlok", + "Findings": "Megállapítások", "Finished %(n)s": "Kész: %(n)s", "Folder": "Mappa", "Free space": "Szabad hely", @@ -1554,6 +1732,7 @@ const LEMBAS_I18N = { "Link it again to give the agent a new token.": "Kapcsolja össze újra, és az ügynök új tokent kap.", "Link it to your server, and agent chats can work in its folders.": "Kapcsolja össze a szerverével, és az ügynökbeszélgetések dolgozhatnak a mappáiban.", "Link this computer": "A számítógép összekapcsolása", + "Loading {{model}}": "{{model}} betöltése", "Loading…": "Betöltés…", "Looking for a LLeMbas server there…": "LLeMbas-kiszolgáló keresése ott…", "MCP tools": "MCP-eszközök", @@ -1605,6 +1784,7 @@ const LEMBAS_I18N = { "Open in the web interface": "Megnyitás a webes felületen", "Open its chat": "Csevegésének megnyitása", "Open its conversation": "A beszélgetésének megnyitása", + "Open slots: %(n)s": "Szabad helyek: %(n)s", "Open the activity panel": "A tevékenységpanel megnyitása", "Open the chat": "A csevegés megnyitása", "Open the page again": "Oldal újbóli megnyitása", @@ -1619,7 +1799,11 @@ const LEMBAS_I18N = { "Preview": "Előnézet", "Project folders": "Projektmappák", "Put the last command and its output into the message box": "Az utolsó parancs és kimenetének beillesztése az üzenetmezőbe", + "Queued": "Sorban áll", + "Queued: {{ahead}} ahead": "Sorban áll: {{ahead}} van előtte", "Read again": "Újraolvasás", + "Reading the prompt": "A prompt olvasása", + "Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "A prompt olvasása: {{done}} / {{total}} token ({{percent}}%)", "Reasoning effort": "Gondolkodási erőfeszítés", "Recommended": "Ajánlott", "Reconnecting…": "Újrakapcsolódás…", @@ -1633,11 +1817,14 @@ const LEMBAS_I18N = { "Result": "Eredmény", "Resume": "Folytatás", "Resume %(title)s": "%(title)s folytatása", + "Retrying in {{in}} s: {{reason}}": "Újrapróbálás {{in}} mp múlva: {{reason}}", "Room": "Szoba", "Run now": "Futtatás most", "Run this in a terminal on the computer this chat works on: the agent's session goes on there, and the chat keeps it.": "Futtassa ezt egy terminálban azon a számítógépen, amelyen ez a csevegés dolgozik: az ügynök munkamenete ott folytatódik, és a csevegés megtartja.", "Run this in a terminal: the agent's session goes on there, in this chat's folder, and the chat keeps it.": "Futtassa ezt egy terminálban: az ügynök munkamenete ott folytatódik, ennek a csevegésnek a mappájában, és a csevegés megtartja.", "Running": "Fut", + "Running {{tool}}": "{{tool}} futtatása", + "Running {{tool}}: {{what}}": "{{tool}} futtatása: {{what}}", "Runs": "Futások", "Save": "Mentés", "Saving starts the reply again; everything after this message is dropped.": "A mentés újraindítja a választ; minden, ami ez után az üzenet után jön, elvész.", @@ -1668,10 +1855,13 @@ const LEMBAS_I18N = { "Stop": "Leállítás", "Stop %(name)s": "%(name)s leállítása", "Stop %(name)s and the members under it? They leave the team.": "Leállítja %(name)s tagot és az alatta lévőket? Kilépnek a csapatból.", + "Stop %(name)s and the subagents under it?": "Leállítod: %(name)s, és az alatta lévő alügynököket?", "Stop the agent": "Ügynök leállítása", "Stopped": "Leállítva", "Stopped.": "Leállítva.", + "Stopped: {{reason}}": "Leállítva: {{reason}}", "Subagent": "Alügynök", + "Subagent %(name)s (%(id)s), %(position)s, asks:": "A(z) %(name)s alügynök (%(id)s), %(position)s, kérdezi:", "Subagents": "Alügynökök", "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.", "System prompt": "Rendszerprompt", @@ -1713,10 +1903,12 @@ const LEMBAS_I18N = { "The terminal could not be loaded.": "A terminált nem sikerült betölteni.", "The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "A terminálhoz újabb LLeMbas-szerver kell. Kérje meg a rendszergazdáját, hogy frissítse.", "These words give no run in the future.": "Ezekből a szavakból nem következik jövőbeli futás.", + "They run on the computer this chat runs on.": "Azon a számítógépen futnak, amelyen ez a beszélgetés fut.", "Think before answering": "Gondolkodjon válasz előtt", "Thinking": "Gondolkodás", "This computer": "Ez a számítógép", "This computer is not linked yet.": "Ez a számítógép még nincs összekapcsolva.", + "This device is not linked right now.": "Ez az eszköz jelenleg nincs összekapcsolva.", "This file is too big to show here (%(size)s).": "Ez a fájl túl nagy a megjelenítéshez (%(size)s).", "This file is too large to send.": "Ez a fájl túl nagy a küldéshez.", "This is a folder; attach the files in it.": "Ez egy mappa; a benne lévő fájlokat csatolja.", @@ -1736,18 +1928,24 @@ const LEMBAS_I18N = { "Up to %(n)s at once": "Legfeljebb %(n)s egyszerre", "Wait until the files are sent.": "Várjon, amíg a fájlok elküldődnek.", "Waiting for a free model": "Szabad modellre vár", + "Waiting for subagents: {{running}} running": "Az alügynökökre vár: {{running}} dolgozik", "Waiting for the browser…": "Várakozás a böngészőre…", "Waiting for the reply before it": "Az előző válaszra vár", + "Waiting for you: {{what}}": "Rád vár: {{what}}", "Waiting for your answer": "Az Ön válaszára vár", + "Waiting for {{model}}: {{reason}}": "Várakozás a(z) {{model}} modellre: {{reason}}", "What it does": "Mit csinál", "What should happen, and when": "Mi történjen, és mikor", "When": "Mikor", "When it matters": "Ha számít", + "Writing": "Írás", + "Writing: {{rate}} tok/s": "Írás: {{rate}} token/s", "Writing…": "Írás…", "Written to %(path)s": "Kiírva ide: %(path)s", "You asked: “%(text)s”": "Az Ön kérése: „%(text)s”", "Your answer": "Az Ön válasza", "Your files on the server, as the chats see them.": "Az Ön fájljai a szerveren, ahogy a beszélgetések látják őket.", + "a question": "egy kérdés", "agent connecting…": "ügynök csatlakozik…", "agent off": "ügynök kikapcsolva", "agent refused": "ügynök elutasítva", @@ -1757,24 +1955,41 @@ const LEMBAS_I18N = { "agent stopped": "ügynök leállt", "agent waiting": "ügynök vár", "agent: no folders": "ügynök: nincs mappa", + "all its slots are in use": "minden helye foglalt", + "allow {{tool}}?": "engedélyezed ezt: {{tool}}?", + "an approval": "egy jóváhagyás", + "budget": "keret", "busy": "foglalt", + "by you": "általad", "caught up": "pótolva", "default": "alapértelmezett", "done": "kész", "failed": "sikertelen", + "findings: %(n)s": "megállapítások: %(n)s", + "finished": "kész", "free": "szabad", + "idle": "tétlen", + "it serves one request at a time and is answering another": "egyszerre egy kérést szolgál ki, és éppen egy másikat válaszol meg", + "its server holds another model now": "a szervere most egy másik modellt tart betöltve", "late": "késve", + "messages: %(n)s": "üzenetek: %(n)s", "not linked": "nincs összekapcsolva", "off": "ki", + "out of context": "elfogyott a kontextus", + "paused: context full": "szüneteltetve: tele a kontextus", "queued": "sorban áll", "report": "jelentés", "run by hand": "kézzel indítva", + "step limit": "lépéskorlát", "stopped": "leállítva", "switched": "váltva", + "the main agent": "a fő ügynök", + "the plan": "a terv", "tried again %(when)s": "újra %(when)s", "update": "frissítés", "update %(version)s": "frissítés: %(version)s", "waiting": "várakozik", + "waiting for a slot": "szabad helyre vár", "window unknown": "az ablak ismeretlen", "working": "dolgozik", "“%(title)s” is deleted on the server, with its messages and files. This cannot be undone.": "„%(title)s” törlődik a szerverről, az üzeneteivel és fájljaival együtt. Ez nem vonható vissza.", @@ -1793,6 +2008,7 @@ const LEMBAS_I18N = { "%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.", "%(percent)s%% of the context window used": "%(percent)s%% della finestra di contesto usato", "%(percent)s%% used": "%(percent)s%% usato", + "%(size)s of %(max)s": "%(size)s di %(max)s", "%(steps)s steps, %(tools)s tools": "%(steps)s passi, %(tools)s strumenti", "%(tokens)s tokens": "%(tokens)s token", "%(tokens)s until auto-compact": "%(tokens)s alla compattazione automatica", @@ -1843,6 +2059,7 @@ const LEMBAS_I18N = { "Command": "Comando", "Commands": "Comandi", "Compact": "Compatta", + "Compacting the conversation": "Compattazione della conversazione", "Compacting…": "Compattazione…", "Compaction buffer": "Riserva per la compattazione", "Compacts before the next reply.": "Si compatta prima della prossima risposta.", @@ -1853,6 +2070,7 @@ const LEMBAS_I18N = { "Connecting…": "Connessione in corso…", "Context": "Contesto", "Context and background tasks": "Contesto e attività in background", + "Context used": "Contesto usato", "Continue in a terminal": "Continua in un terminale", "Continue in a terminal…": "Continua in un terminale…", "Copied": "Copiato", @@ -1882,12 +2100,15 @@ const LEMBAS_I18N = { "Edit message": "Modifica messaggio", "Edit — asks before commands": "Edit — chiede prima dei comandi", "Empty folder": "Cartella vuota", + "Ended": "Terminati", "Environment": "Ambiente", "Estimated: no reply has been counted yet.": "Stima: nessuna risposta è stata ancora contata.", "Every result, even with nothing to report.": "Ogni risultato, anche senza nulla da segnalare.", "Every weekday at 8:00, summarise the news on my reading list": "Ogni giorno feriale alle 8:00, riassumi le notizie della mia lista di lettura", "Failed": "Non riusciti", + "Failed: {{reason}}": "Non riuscito: {{reason}}", "Files": "File", + "Findings": "Risultati", "Finished %(n)s": "Terminate: %(n)s", "Folder": "Cartella", "Free space": "Spazio libero", @@ -1908,6 +2129,7 @@ const LEMBAS_I18N = { "Link it again to give the agent a new token.": "Collegalo di nuovo per dare all’agente un nuovo token.", "Link it to your server, and agent chats can work in its folders.": "Collegalo al tuo server e le chat dell’agente potranno lavorare nelle sue cartelle.", "Link this computer": "Collega questo computer", + "Loading {{model}}": "Caricamento di {{model}}", "Loading…": "Caricamento…", "Looking for a LLeMbas server there…": "Cerco un server LLeMbas lì…", "MCP tools": "Strumenti MCP", @@ -1959,6 +2181,7 @@ const LEMBAS_I18N = { "Open in the web interface": "Apri nell'interfaccia web", "Open its chat": "Apri la sua chat", "Open its conversation": "Apri la sua conversazione", + "Open slots: %(n)s": "Posti liberi: %(n)s", "Open the activity panel": "Apri il pannello attività", "Open the chat": "Apri la chat", "Open the page again": "Riapri la pagina", @@ -1973,7 +2196,11 @@ const LEMBAS_I18N = { "Preview": "Anteprima", "Project folders": "Cartelle dei progetti", "Put the last command and its output into the message box": "Metti l'ultimo comando e il suo output nella casella del messaggio", + "Queued": "In coda", + "Queued: {{ahead}} ahead": "In coda: {{ahead}} davanti", "Read again": "Rileggi", + "Reading the prompt": "Lettura del prompt", + "Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Lettura del prompt: {{done}} / {{total}} token ({{percent}}%)", "Reasoning effort": "Sforzo di ragionamento", "Recommended": "Consigliato", "Reconnecting…": "Riconnessione…", @@ -1987,11 +2214,14 @@ const LEMBAS_I18N = { "Result": "Risultato", "Resume": "Riprendi", "Resume %(title)s": "Riprendi %(title)s", + "Retrying in {{in}} s: {{reason}}": "Nuovo tentativo tra {{in}} s: {{reason}}", "Room": "Stanza", "Run now": "Esegui ora", "Run this in a terminal on the computer this chat works on: the agent's session goes on there, and the chat keeps it.": "Eseguilo in un terminale sul computer su cui lavora questa chat: la sessione dell'agente prosegue lì e la chat la conserva.", "Run this in a terminal: the agent's session goes on there, in this chat's folder, and the chat keeps it.": "Eseguilo in un terminale: la sessione dell'agente prosegue lì, nella cartella di questa chat, e la chat la conserva.", "Running": "In esecuzione", + "Running {{tool}}": "Esecuzione di {{tool}}", + "Running {{tool}}: {{what}}": "Esecuzione di {{tool}}: {{what}}", "Runs": "Esecuzioni", "Save": "Salva", "Saving starts the reply again; everything after this message is dropped.": "Salvando, la risposta riparte; tutto ciò che segue questo messaggio viene scartato.", @@ -2022,10 +2252,13 @@ const LEMBAS_I18N = { "Stop": "Ferma", "Stop %(name)s": "Ferma %(name)s", "Stop %(name)s and the members under it? They leave the team.": "Fermare %(name)s e i membri sotto di lui? Lasciano la squadra.", + "Stop %(name)s and the subagents under it?": "Fermare %(name)s e i subagenti sotto di esso?", "Stop the agent": "Ferma l'agente", "Stopped": "Fermato", "Stopped.": "Interrotto.", + "Stopped: {{reason}}": "Fermato: {{reason}}", "Subagent": "Sottoagente", + "Subagent %(name)s (%(id)s), %(position)s, asks:": "Il subagente %(name)s (%(id)s), %(position)s, chiede:", "Subagents": "Sottoagenti", "Summarise the earlier turns now and send the summary in their place.": "Riassumi ora i turni precedenti e invia il riassunto al loro posto.", "System prompt": "Prompt di sistema", @@ -2067,10 +2300,12 @@ const LEMBAS_I18N = { "The terminal could not be loaded.": "Impossibile caricare il terminale.", "The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Il terminale richiede un server LLeMbas più recente. Chiedi al suo amministratore di aggiornarlo.", "These words give no run in the future.": "Queste parole non danno nessuna esecuzione futura.", + "They run on the computer this chat runs on.": "Girano sul computer su cui gira questa chat.", "Think before answering": "Ragiona prima di rispondere", "Thinking": "Ragionamento", "This computer": "Questo computer", "This computer is not linked yet.": "Questo computer non è ancora collegato.", + "This device is not linked right now.": "Questo dispositivo non è collegato in questo momento.", "This file is too big to show here (%(size)s).": "Questo file è troppo grande per essere mostrato (%(size)s).", "This file is too large to send.": "Questo file è troppo grande per essere inviato.", "This is a folder; attach the files in it.": "Questa è una cartella; alleghi i file che contiene.", @@ -2090,18 +2325,24 @@ const LEMBAS_I18N = { "Up to %(n)s at once": "Fino a %(n)s alla volta", "Wait until the files are sent.": "Attenda che i file siano inviati.", "Waiting for a free model": "In attesa di un modello libero", + "Waiting for subagents: {{running}} running": "In attesa dei subagenti: {{running}} al lavoro", "Waiting for the browser…": "In attesa del browser…", "Waiting for the reply before it": "In attesa della risposta precedente", + "Waiting for you: {{what}}": "In attesa di te: {{what}}", "Waiting for your answer": "In attesa della sua risposta", + "Waiting for {{model}}: {{reason}}": "In attesa di {{model}}: {{reason}}", "What it does": "Cosa fa", "What should happen, and when": "Cosa deve succedere, e quando", "When": "Quando", "When it matters": "Quando conta", + "Writing": "Scrittura", + "Writing: {{rate}} tok/s": "Scrittura: {{rate}} tok/s", "Writing…": "Sta scrivendo…", "Written to %(path)s": "Scritto in %(path)s", "You asked: “%(text)s”": "Hai chiesto: «%(text)s»", "Your answer": "La sua risposta", "Your files on the server, as the chats see them.": "I tuoi file sul server, come li vedono le chat.", + "a question": "una domanda", "agent connecting…": "agente in collegamento…", "agent off": "agente spento", "agent refused": "agente rifiutato", @@ -2111,24 +2352,41 @@ const LEMBAS_I18N = { "agent stopped": "agente fermato", "agent waiting": "agente in attesa", "agent: no folders": "agente: nessuna cartella", + "all its slots are in use": "tutti i suoi posti sono occupati", + "allow {{tool}}?": "consentire {{tool}}?", + "an approval": "un’approvazione", + "budget": "budget", "busy": "occupato", + "by you": "da te", "caught up": "recuperata", "default": "predefinito", "done": "finito", "failed": "fallito", + "findings: %(n)s": "risultati: %(n)s", + "finished": "finito", "free": "libero", + "idle": "inattivo", + "it serves one request at a time and is answering another": "serve una richiesta alla volta e ne sta servendo un’altra", + "its server holds another model now": "il suo server ha ora un altro modello", "late": "in ritardo", + "messages: %(n)s": "messaggi: %(n)s", "not linked": "non collegato", "off": "disattivato", + "out of context": "senza contesto", + "paused: context full": "in pausa: contesto pieno", "queued": "in coda", "report": "resoconto", "run by hand": "avviata a mano", + "step limit": "limite di passi", "stopped": "interrotto", "switched": "cambiato", + "the main agent": "l’agente principale", + "the plan": "il piano", "tried again %(when)s": "di nuovo %(when)s", "update": "aggiorna", "update %(version)s": "aggiorna a %(version)s", "waiting": "in attesa", + "waiting for a slot": "in attesa di un posto", "window unknown": "finestra sconosciuta", "working": "al lavoro", "“%(title)s” is deleted on the server, with its messages and files. This cannot be undone.": "«%(title)s» viene eliminata dal server, con i suoi messaggi e file. Non si può annullare.", @@ -2147,6 +2405,7 @@ const LEMBAS_I18N = { "%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.", "%(percent)s%% of the context window used": "Użyto %(percent)s%% okna kontekstu", "%(percent)s%% used": "Użyto: %(percent)s%%", + "%(size)s of %(max)s": "%(size)s z %(max)s", "%(steps)s steps, %(tools)s tools": "Kroki: %(steps)s, narzędzia: %(tools)s", "%(tokens)s tokens": "Tokeny: %(tokens)s", "%(tokens)s until auto-compact": "%(tokens)s do automatycznego streszczenia", @@ -2197,6 +2456,7 @@ const LEMBAS_I18N = { "Command": "Polecenie", "Commands": "Polecenia", "Compact": "Streść", + "Compacting the conversation": "Kompaktowanie rozmowy", "Compacting…": "Streszczanie…", "Compaction buffer": "Rezerwa na kompakcję", "Compacts before the next reply.": "Zostanie streszczona przed następną odpowiedzią.", @@ -2207,6 +2467,7 @@ const LEMBAS_I18N = { "Connecting…": "Łączenie…", "Context": "Kontekst", "Context and background tasks": "Kontekst i zadania w tle", + "Context used": "Użyty kontekst", "Continue in a terminal": "Kontynuuj w terminalu", "Continue in a terminal…": "Kontynuuj w terminalu…", "Copied": "Skopiowano", @@ -2236,12 +2497,15 @@ const LEMBAS_I18N = { "Edit message": "Edytuj wiadomość", "Edit — asks before commands": "Edit — pyta przed poleceniami", "Empty folder": "Pusty folder", + "Ended": "Zakończone", "Environment": "Środowisko", "Estimated: no reply has been counted yet.": "Szacunek: żadna odpowiedź nie została jeszcze policzona.", "Every result, even with nothing to report.": "Każdy wynik, nawet bez niczego do zgłoszenia.", "Every weekday at 8:00, summarise the news on my reading list": "W każdy dzień roboczy o 8:00 streść wiadomości z mojej listy do czytania", "Failed": "Nieudane", + "Failed: {{reason}}": "Niepowodzenie: {{reason}}", "Files": "Pliki", + "Findings": "Ustalenia", "Finished %(n)s": "Zakończone: %(n)s", "Folder": "Folder", "Free space": "Wolne miejsce", @@ -2262,6 +2526,7 @@ const LEMBAS_I18N = { "Link it again to give the agent a new token.": "Połącz go ponownie, aby agent dostał nowy token.", "Link it to your server, and agent chats can work in its folders.": "Połącz go ze swoim serwerem, a czaty agenta będą mogły pracować w jego folderach.", "Link this computer": "Połącz ten komputer", + "Loading {{model}}": "Wczytywanie {{model}}", "Loading…": "Wczytywanie…", "Looking for a LLeMbas server there…": "Szukam tam serwera LLeMbas…", "MCP tools": "Narzędzia MCP", @@ -2313,6 +2578,7 @@ const LEMBAS_I18N = { "Open in the web interface": "Otwórz w interfejsie WWW", "Open its chat": "Otwórz jego czat", "Open its conversation": "Otwórz jego rozmowę", + "Open slots: %(n)s": "Wolne miejsca: %(n)s", "Open the activity panel": "Otwórz panel aktywności", "Open the chat": "Otwórz czat", "Open the page again": "Otwórz stronę ponownie", @@ -2327,7 +2593,11 @@ const LEMBAS_I18N = { "Preview": "Podgląd", "Project folders": "Foldery projektów", "Put the last command and its output into the message box": "Wstaw ostatnie polecenie i jego wynik do pola wiadomości", + "Queued": "W kolejce", + "Queued: {{ahead}} ahead": "W kolejce: przed nim {{ahead}}", "Read again": "Wczytaj ponownie", + "Reading the prompt": "Czytanie promptu", + "Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Czytanie promptu: {{done}} / {{total}} tokenów ({{percent}}%)", "Reasoning effort": "Poziom rozumowania", "Recommended": "Zalecane", "Reconnecting…": "Ponowne łączenie…", @@ -2341,11 +2611,14 @@ const LEMBAS_I18N = { "Result": "Wynik", "Resume": "Wznów", "Resume %(title)s": "Wznów %(title)s", + "Retrying in {{in}} s: {{reason}}": "Ponowna próba za {{in}} s: {{reason}}", "Room": "Pokój", "Run now": "Uruchom teraz", "Run this in a terminal on the computer this chat works on: the agent's session goes on there, and the chat keeps it.": "Uruchom to w terminalu na komputerze, na którym pracuje ten czat: sesja agenta będzie tam kontynuowana, a czat ją zachowa.", "Run this in a terminal: the agent's session goes on there, in this chat's folder, and the chat keeps it.": "Uruchom to w terminalu: sesja agenta będzie tam kontynuowana, w folderze tego czatu, a czat ją zachowa.", "Running": "Działa", + "Running {{tool}}": "Uruchamianie {{tool}}", + "Running {{tool}}: {{what}}": "Uruchamianie {{tool}}: {{what}}", "Runs": "Uruchomienia", "Save": "Zapisz", "Saving starts the reply again; everything after this message is dropped.": "Zapisanie uruchamia odpowiedź od nowa; wszystko po tej wiadomości zostanie odrzucone.", @@ -2376,10 +2649,13 @@ const LEMBAS_I18N = { "Stop": "Zatrzymać", "Stop %(name)s": "Zatrzymaj %(name)s", "Stop %(name)s and the members under it? They leave the team.": "Zatrzymać %(name)s i członków pod nim? Opuszczą zespół.", + "Stop %(name)s and the subagents under it?": "Zatrzymać %(name)s i podagentów pod nim?", "Stop the agent": "Zatrzymaj agenta", "Stopped": "Zatrzymano", "Stopped.": "Zatrzymano.", + "Stopped: {{reason}}": "Zatrzymano: {{reason}}", "Subagent": "Podagent", + "Subagent %(name)s (%(id)s), %(position)s, asks:": "Podagent %(name)s (%(id)s), %(position)s, pyta:", "Subagents": "Podagenci", "Summarise the earlier turns now and send the summary in their place.": "Streścić teraz wcześniejsze tury i wysyłać streszczenie zamiast nich.", "System prompt": "Prompt systemowy", @@ -2421,10 +2697,12 @@ const LEMBAS_I18N = { "The terminal could not be loaded.": "Nie udało się wczytać terminala.", "The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Terminal wymaga nowszego serwera LLeMbas. Poproś jego administratora o aktualizację.", "These words give no run in the future.": "Z tych słów nie wynika żadne przyszłe uruchomienie.", + "They run on the computer this chat runs on.": "Działają na komputerze, na którym działa ta rozmowa.", "Think before answering": "Rozumuj przed odpowiedzią", "Thinking": "Rozumowanie", "This computer": "Ten komputer", "This computer is not linked yet.": "Ten komputer nie jest jeszcze połączony.", + "This device is not linked right now.": "To urządzenie nie jest teraz połączone.", "This file is too big to show here (%(size)s).": "Ten plik jest za duży, by go tu pokazać (%(size)s).", "This file is too large to send.": "Ten plik jest za duży, aby go wysłać.", "This is a folder; attach the files in it.": "To jest folder; dołącz pliki, które zawiera.", @@ -2444,18 +2722,24 @@ const LEMBAS_I18N = { "Up to %(n)s at once": "Do %(n)s naraz", "Wait until the files are sent.": "Poczekaj, aż pliki zostaną wysłane.", "Waiting for a free model": "Czeka na wolny model", + "Waiting for subagents: {{running}} running": "Czeka na podagentów: pracuje {{running}}", "Waiting for the browser…": "Czekam na przeglądarkę…", "Waiting for the reply before it": "Czeka na odpowiedź przed nią", + "Waiting for you: {{what}}": "Czeka na ciebie: {{what}}", "Waiting for your answer": "Czeka na Twoją odpowiedź", + "Waiting for {{model}}: {{reason}}": "Czekam na {{model}}: {{reason}}", "What it does": "Co robi", "What should happen, and when": "Co ma się stać i kiedy", "When": "Kiedy", "When it matters": "Gdy to ważne", + "Writing": "Pisanie", + "Writing: {{rate}} tok/s": "Pisanie: {{rate}} tok/s", "Writing…": "Pisze…", "Written to %(path)s": "Zapisano do %(path)s", "You asked: “%(text)s”": "Twoja prośba: „%(text)s”", "Your answer": "Twoja odpowiedź", "Your files on the server, as the chats see them.": "Twoje pliki na serwerze, tak jak widzą je czaty.", + "a question": "pytanie", "agent connecting…": "agent łączy się…", "agent off": "agent wyłączony", "agent refused": "agent odrzucony", @@ -2465,24 +2749,41 @@ const LEMBAS_I18N = { "agent stopped": "agent zatrzymany", "agent waiting": "agent czeka", "agent: no folders": "agent: brak folderów", + "all its slots are in use": "wszystkie jego miejsca są zajęte", + "allow {{tool}}?": "zezwolić na {{tool}}?", + "an approval": "zgoda", + "budget": "budżet", "busy": "zajęty", + "by you": "przez ciebie", "caught up": "nadrobiony", "default": "domyślna", "done": "gotowe", "failed": "nie powiodło się", + "findings: %(n)s": "ustalenia: %(n)s", + "finished": "zakończony", "free": "wolny", + "idle": "bezczynny", + "it serves one request at a time and is answering another": "obsługuje jedno żądanie naraz i właśnie obsługuje inne", + "its server holds another model now": "jego serwer ma teraz wczytany inny model", "late": "z opóźnieniem", + "messages: %(n)s": "wiadomości: %(n)s", "not linked": "niepołączony", "off": "wył.", + "out of context": "bez kontekstu", + "paused: context full": "wstrzymany: pełny kontekst", "queued": "w kolejce", "report": "raport", "run by hand": "uruchomiony ręcznie", + "step limit": "limit kroków", "stopped": "zatrzymane", "switched": "zmieniony", + "the main agent": "główny agent", + "the plan": "plan", "tried again %(when)s": "ponownie %(when)s", "update": "aktualizacja", "update %(version)s": "aktualizacja %(version)s", "waiting": "czeka", + "waiting for a slot": "czeka na wolne miejsce", "window unknown": "okno nieznane", "working": "pracuje", "“%(title)s” is deleted on the server, with its messages and files. This cannot be undone.": "„%(title)s” zostanie usunięty z serwera wraz z wiadomościami i plikami. Tego nie można cofnąć.", @@ -2501,6 +2802,7 @@ const LEMBAS_I18N = { "%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.", "%(percent)s%% of the context window used": "Využité: %(percent)s %% kontextového okna", "%(percent)s%% used": "Využité: %(percent)s %%", + "%(size)s of %(max)s": "%(size)s z %(max)s", "%(steps)s steps, %(tools)s tools": "Kroky: %(steps)s, nástroje: %(tools)s", "%(tokens)s tokens": "%(tokens)s tokenov", "%(tokens)s until auto-compact": "%(tokens)s do automatického zhrnutia", @@ -2551,6 +2853,7 @@ const LEMBAS_I18N = { "Command": "Príkaz", "Commands": "Príkazy", "Compact": "Zhrnúť", + "Compacting the conversation": "Zhŕňa sa konverzácia", "Compacting…": "Zhŕňa sa…", "Compaction buffer": "Rezerva na zhustenie", "Compacts before the next reply.": "Zhrnie sa pred ďalšou odpoveďou.", @@ -2561,6 +2864,7 @@ const LEMBAS_I18N = { "Connecting…": "Pripája sa…", "Context": "Kontext", "Context and background tasks": "Kontext a úlohy na pozadí", + "Context used": "Využitý kontext", "Continue in a terminal": "Pokračovať v termináli", "Continue in a terminal…": "Pokračovať v termináli…", "Copied": "Skopírované", @@ -2590,12 +2894,15 @@ const LEMBAS_I18N = { "Edit message": "Upraviť správu", "Edit — asks before commands": "Edit — pýta sa pred príkazmi", "Empty folder": "Prázdny priečinok", + "Ended": "Skončené", "Environment": "Prostredie", "Estimated: no reply has been counted yet.": "Odhad: žiadna odpoveď ešte nebola spočítaná.", "Every result, even with nothing to report.": "Každý výsledok, aj keď nie je čo hlásiť.", "Every weekday at 8:00, summarise the news on my reading list": "Každý pracovný deň o 8:00 zhrň správy z môjho zoznamu na čítanie", "Failed": "Zlyhalo", + "Failed: {{reason}}": "Zlyhalo: {{reason}}", "Files": "Súbory", + "Findings": "Zistenia", "Finished %(n)s": "Dokončené: %(n)s", "Folder": "Priečinok", "Free space": "Voľné miesto", @@ -2616,6 +2923,7 @@ const LEMBAS_I18N = { "Link it again to give the agent a new token.": "Prepojte ho znova a agent dostane nový token.", "Link it to your server, and agent chats can work in its folders.": "Prepojte ho so svojím serverom a agentné konverzácie budú môcť pracovať v jeho priečinkoch.", "Link this computer": "Prepojiť tento počítač", + "Loading {{model}}": "Načítava sa {{model}}", "Loading…": "Načítava sa…", "Looking for a LLeMbas server there…": "Hľadám tam server LLeMbas…", "MCP tools": "Nástroje MCP", @@ -2667,6 +2975,7 @@ const LEMBAS_I18N = { "Open in the web interface": "Otvoriť vo webovom rozhraní", "Open its chat": "Otvoriť jeho konverzáciu", "Open its conversation": "Otvoriť jeho konverzáciu", + "Open slots: %(n)s": "Voľné miesta: %(n)s", "Open the activity panel": "Otvoriť panel aktivity", "Open the chat": "Otvoriť konverzáciu", "Open the page again": "Otvoriť stránku znova", @@ -2681,7 +2990,11 @@ const LEMBAS_I18N = { "Preview": "Náhľad", "Project folders": "Priečinky projektov", "Put the last command and its output into the message box": "Vložiť posledný príkaz a jeho výstup do políčka správy", + "Queued": "Vo fronte", + "Queued: {{ahead}} ahead": "Vo fronte: pred ním {{ahead}}", "Read again": "Načítať znova", + "Reading the prompt": "Číta sa výzva", + "Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Číta sa výzva: {{done}} / {{total}} tokenov ({{percent}} %)", "Reasoning effort": "Náročnosť uvažovania", "Recommended": "Odporúčané", "Reconnecting…": "Znova sa pripája…", @@ -2695,11 +3008,14 @@ const LEMBAS_I18N = { "Result": "Výsledok", "Resume": "Pokračovať", "Resume %(title)s": "Obnoviť %(title)s", + "Retrying in {{in}} s: {{reason}}": "Nový pokus o {{in}} s: {{reason}}", "Room": "Miestnosť", "Run now": "Spustiť teraz", "Run this in a terminal on the computer this chat works on: the agent's session goes on there, and the chat keeps it.": "Spustite to v termináli na počítači, na ktorom táto konverzácia pracuje: relácia agenta tam pokračuje a konverzácia si ju ponechá.", "Run this in a terminal: the agent's session goes on there, in this chat's folder, and the chat keeps it.": "Spustite to v termináli: relácia agenta tam pokračuje, v priečinku tejto konverzácie, a konverzácia si ju ponechá.", "Running": "Beží", + "Running {{tool}}": "Spúšťa {{tool}}", + "Running {{tool}}: {{what}}": "Spúšťa {{tool}}: {{what}}", "Runs": "Spustenia", "Save": "Uložiť", "Saving starts the reply again; everything after this message is dropped.": "Uložením sa odpoveď spustí znova; všetko po tejto správe sa zahodí.", @@ -2730,10 +3046,13 @@ const LEMBAS_I18N = { "Stop": "Zastaviť", "Stop %(name)s": "Zastaviť %(name)s", "Stop %(name)s and the members under it? They leave the team.": "Zastaviť %(name)s a členov pod ním? Opustia tím.", + "Stop %(name)s and the subagents under it?": "Zastaviť %(name)s a podagentov pod ním?", "Stop the agent": "Zastaviť agenta", "Stopped": "Zastavené", "Stopped.": "Zastavené.", + "Stopped: {{reason}}": "Zastavené: {{reason}}", "Subagent": "Podagent", + "Subagent %(name)s (%(id)s), %(position)s, asks:": "Podagent %(name)s (%(id)s), %(position)s, sa pýta:", "Subagents": "Podagenti", "Summarise the earlier turns now and send the summary in their place.": "Zhrnúť skoršie kolá hneď a posielať namiesto nich zhrnutie.", "System prompt": "Systémový pokyn", @@ -2775,10 +3094,12 @@ const LEMBAS_I18N = { "The terminal could not be loaded.": "Terminál sa nepodarilo načítať.", "The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Terminál potrebuje novší server LLeMbas. Požiadajte jeho správcu o aktualizáciu.", "These words give no run in the future.": "Z týchto slov nevychádza žiadne budúce spustenie.", + "They run on the computer this chat runs on.": "Bežia na počítači, na ktorom beží táto konverzácia.", "Think before answering": "Premýšľať pred odpoveďou", "Thinking": "Premýšľa", "This computer": "Tento počítač", "This computer is not linked yet.": "Tento počítač ešte nie je prepojený.", + "This device is not linked right now.": "Toto zariadenie teraz nie je pripojené.", "This file is too big to show here (%(size)s).": "Tento súbor je príliš veľký na zobrazenie (%(size)s).", "This file is too large to send.": "Tento súbor je príliš veľký na odoslanie.", "This is a folder; attach the files in it.": "Toto je priečinok; priložte súbory v ňom.", @@ -2798,18 +3119,24 @@ const LEMBAS_I18N = { "Up to %(n)s at once": "Najviac %(n)s naraz", "Wait until the files are sent.": "Počkajte, kým sa súbory odošlú.", "Waiting for a free model": "Čaká na voľný model", + "Waiting for subagents: {{running}} running": "Čaká na podagentov: pracujú {{running}}", "Waiting for the browser…": "Čaká sa na prehliadač…", "Waiting for the reply before it": "Čaká na odpoveď pred ňou", + "Waiting for you: {{what}}": "Čaká na vás: {{what}}", "Waiting for your answer": "Čaká na vašu odpoveď", + "Waiting for {{model}}: {{reason}}": "Čaká sa na {{model}}: {{reason}}", "What it does": "Čo robí", "What should happen, and when": "Čo sa má stať a kedy", "When": "Kedy", "When it matters": "Keď na tom záleží", + "Writing": "Píše", + "Writing: {{rate}} tok/s": "Píše: {{rate}} tok/s", "Writing…": "Píše sa…", "Written to %(path)s": "Zapísané do %(path)s", "You asked: “%(text)s”": "Požiadali ste: „%(text)s“", "Your answer": "Vaša odpoveď", "Your files on the server, as the chats see them.": "Vaše súbory na serveri, tak ako ich vidia konverzácie.", + "a question": "otázka", "agent connecting…": "agent sa pripája…", "agent off": "agent vypnutý", "agent refused": "agent odmietnutý", @@ -2819,24 +3146,41 @@ const LEMBAS_I18N = { "agent stopped": "agent zastavený", "agent waiting": "agent čaká", "agent: no folders": "agent: žiadne priečinky", + "all its slots are in use": "všetky jeho sloty sú obsadené", + "allow {{tool}}?": "povoliť {{tool}}?", + "an approval": "povolenie", + "budget": "rozpočet", "busy": "zaneprázdnený", + "by you": "vami", "caught up": "dohnané", "default": "predvolené", "done": "hotovo", "failed": "zlyhalo", + "findings: %(n)s": "zistenia: %(n)s", + "finished": "dokončený", "free": "voľný", + "idle": "nečinný", + "it serves one request at a time and is answering another": "vybavuje jednu požiadavku naraz a práve vybavuje inú", + "its server holds another model now": "jeho server má teraz načítaný iný model", "late": "s oneskorením", + "messages: %(n)s": "správy: %(n)s", "not linked": "neprepojené", "off": "vypnuté", + "out of context": "bez kontextu", + "paused: context full": "pozastavený: plný kontext", "queued": "v rade", "report": "hlásenie", "run by hand": "spustené ručne", + "step limit": "limit krokov", "stopped": "zastavené", "switched": "prepnuté", + "the main agent": "hlavný agent", + "the plan": "plán", "tried again %(when)s": "znova %(when)s", "update": "aktualizácia", "update %(version)s": "aktualizácia %(version)s", "waiting": "čaká", + "waiting for a slot": "čaká na voľný slot", "window unknown": "okno neznáme", "working": "pracuje", "“%(title)s” is deleted on the server, with its messages and files. This cannot be undone.": "„%(title)s“ sa zmaže na serveri aj so svojimi správami a súbormi. Nedá sa to vrátiť.", diff --git a/ui/js/activity.js b/ui/js/activity.js index ce36333..9c5ee98 100644 --- a/ui/js/activity.js +++ b/ui/js/activity.js @@ -13,6 +13,14 @@ // GET /chats/{chat}/agents/{kind}/{id} one task and its conversation // POST /chats/{chat}/agents/{kind}/{id}/stop // POST /chats/{chat}/compact summarise the earlier turns now +// GET /chats/{chat}/tree?since= the subagent tree (spec 3.4.0) and the room +// GET /chats/{chat}/subagents/{id}/transcript a subagent's conversation, read-only +// GET /chats/{chat}/subagents/{id}/findings what it kept +// POST /chats/{chat}/subagents/{id}/stop it and those under it +// +// The tree comes too as the reply stream's `tree` frame (chat.js hands it on). An agent chat's +// subagents run on its device: while the device is away the server sends the tree it kept +// (`live` false) and answers 409 for a conversation or findings, in its own words. // // 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 @@ -27,6 +35,7 @@ import { get, post, follow, why } from "./bridge.js" import { markdown } from "./markdown.js" import { ask } from "./popup.js" import { openServerFile } from "./explorer.js" +import { treePart, tickTree, treeState, TREE_LIVE } from "./tree.js" const ACTIVE = ["queued", "working", "waiting"] // How many live agents show before "Show N more", as on the server's panel. @@ -50,7 +59,12 @@ let readAt = 0 // when `data` was read: the elapsed times count on from there let contextAt = 0 let reading = null // the read in flight let readAgain = "" // "", "plain" or "context": what to read again once the read in flight is done -let view = null // null: the panel; { kind, id, name }: one agent's conversation +let view = null // null: the panel; { kind, id, name }: one agent's conversation ({ kind: "sub", what } a subagent's) +let tree = null // the subagent tree (spec 3.4.0), null for none or an older server +let treeRoom = [] // the room as the tree's answer gives it (an agent chat's comes only there) +let treeLive = true // false: an agent chat's device is away, and this is the tree the server kept +let treeNote = "" // why the last stop did not go through +let subTimer = 0 // a live subagent's conversation, read again while it works const openRows = new Set() // agents unfolded, by kind:id; sections folded, by name let ticker = 0 @@ -147,6 +161,10 @@ export function watchActivity(next) { data = null room = [] cursor = 0 + tree = null + treeRoom = [] + treeLive = true + treeNote = "" readAt = contextAt = 0 view = null compactError = "" @@ -164,6 +182,15 @@ export function watchActivity(next) { } } +/** The reply stream's `tree` frame: the tree as it is now. */ +export function treeChanged(chatId, next) { + if (!chat || chat.id !== chatId) return + tree = next || null + if (!panelOpen()) return + if (!view) render() + else if (view.kind === "sub" && view.what === "transcript") readAgent() +} + /** A reply has ended, or the chat's settings changed: the context and the outputs move with it. */ export function activityChanged() { if (chat && panelOpen()) read(true) @@ -241,8 +268,16 @@ async function read(withContext) { const id = chat.id reading = (async () => { try { - const a = (await get(`/chats/${enc(id)}/activity?since=${cursor}&context=${withContext ? "true" : "false"}`)).activity + const [r, tr] = await Promise.all([ + get(`/chats/${enc(id)}/activity?since=${cursor}&context=${withContext ? "true" : "false"}`), + // A server before 3.4.0 has no tree: 404, and the panel is as it was. + get(`/chats/${enc(id)}/tree`).catch(() => null), + ]) + const a = r.activity if (chat?.id !== id || !a) return + tree = tr?.tree || null + treeRoom = tr?.room || [] + treeLive = tr ? tr.live !== false : true if (!withContext) a.context = data?.context ?? null else contextAt = Date.now() if (a.team) { @@ -363,6 +398,7 @@ function tick() { for (const el of document.querySelectorAll("#activity .agent__time[data-active]")) { el.textContent = duration(Number(el.dataset.seconds) + extra) } + tickTree($("activity-body")) } function renderError(text) { @@ -392,13 +428,15 @@ function render() { } // Kept where the reader was: the panel is drawn again on every change. const scroll = body.scrollTop - const entries = data.agents || [] + // The tree draws the subagents; a team's members are not drawn again among the tasks. + const entries = (data.agents || []).filter((e) => !(tree && e.kind === "member")) const parts = [ chat?.kind === "agent" && chat.device ? section("environment", t("Environment"), 0, environmentPart(chat)) : null, section("context", t("Context"), 0, contextPart(data.context)), section("outputs", t("Outputs"), (data.outputs || []).length, outputsPart(data.outputs || [])), + tree ? section("subagents", t("Subagents"), 0, subagentsPart()) : null, section("tasks", t("Background tasks"), 0, tasksPart(entries)), - data.team ? section("room", t("Room"), room.length, roomPart()) : null, + data.team || treeRoom.length ? section("room", t("Room"), roomList().length, roomPart()) : null, data.plan ? section("plan", t("Plan"), 0, h("div", { class: "activity__plan" }, planList(data.plan))) : null, ] body.replaceChildren(...parts.filter(Boolean)) @@ -561,7 +599,7 @@ function outputsPart(outputs) { } function tasksPart(entries) { - const team = data.team + const team = tree ? null : data.team const box = h("div", { class: "tasks", "aria-live": "polite" }) if (!entries.length && !team) { box.append(h("p", { class: "activity__empty", text: t("Nothing runs in the background. Subagents, a team's members and background commands show here while they work.") })) @@ -584,7 +622,84 @@ function tasksPart(entries) { return box } +/** The room: as the activity read it (a chat's team), else as the tree's answer gave it. */ +function roomList() { + return room.length ? room : treeRoom +} + +function subagentsPart() { + const box = treePart(tree, { + room: roomList(), + device: chat?.kind === "agent", + act: { + open: (a) => openSub(a, "transcript"), + findings: (a) => openSub(a, "findings"), + stop: stopSub, + isOpen: (key) => openRows.has(key), + toggled: (key, on) => (on ? openRows.add(key) : openRows.delete(key)), + }, + }) + if (!treeLive) box.prepend(h("p", { class: "activity__empty", text: t("This device is not linked right now.") })) + if (treeNote) box.prepend(h("p", { class: "activity__empty is-error", text: treeNote })) + return box +} + +async function stopSub(a) { + if (!chat) return + const yes = await ask({ title: t("Stop %(name)s", { name: a.name }), text: t("Stop %(name)s and the subagents under it?", { name: a.name }), ok: t("Stop"), cancel: t("Cancel"), danger: true }) + if (!yes) return + treeNote = "" + try { + await post(`/chats/${enc(chat.id)}/subagents/${enc(a.id)}/stop`) + } catch (err) { + treeNote = why(err) + } + read(false) +} + +function openSub(a, what) { + view = { kind: "sub", id: a.id, name: a.name || a.id, what } + $("activity").dataset.view = "agent" + $("activity-title").textContent = what === "findings" ? `${view.name} · ${t("Findings")}` : view.name + $("activity-back").hidden = false + clear($("activity-body")).append(h("p", { class: "activity__empty", text: t("Loading…") })) + readAgent() +} + +/** A subagent's conversation or findings, read-only; a live one's conversation is read again while it works. */ +async function readSub() { + clearTimeout(subTimer) + const { id, what } = view + let r + try { + r = await get(`/chats/${enc(chat.id)}/subagents/${enc(id)}/${what}`) + } catch (err) { + // 409: an agent chat's device is away -- the server says it is on the device. + if (view?.id === id && view.what === what) clear($("activity-body")).append(h("p", { class: "activity__empty is-error", text: why(err) })) + return + } + if (view?.kind !== "sub" || view.id !== id || view.what !== what) return + const body = $("activity-body") + if (what === "findings") { + const list = r.findings || [] + body.replaceChildren( + list.length + ? h("ol", { class: "findings" }, list.map((f) => h("li", { class: "findings__item", value: String(f.n) }, h("div", { class: "md" }, markdown(String(f.text || "")))))) + : h("p", { class: "activity__empty", text: t("Nothing yet.") }), + ) + return + } + const atEnd = body.scrollHeight - body.scrollTop - body.clientHeight < 40 + const live = TREE_LIVE.includes(r.state) + const head = h("div", { class: "convo__head" }, h("p", { class: "agent__facts" }, h("span", { class: "agent__state", text: treeState(r.state) }))) + const turns = turnsOf(r.turns || []) + body.replaceChildren(head, h("div", { class: "convo" }, turns.length ? turns : h("p", { class: "activity__empty", text: t("Nothing yet.") }))) + if (atEnd) body.scrollTop = body.scrollHeight + if (live) subTimer = setTimeout(() => view?.kind === "sub" && view.id === id && panelOpen() && readAgent(), 3000) +} + function roomPart() { + const room = roomList() if (!room.length) return h("p", { class: "activity__empty", text: t("Nothing has been said in the room yet.") }) return h( "ol", @@ -747,6 +862,7 @@ function openAgent(a) { } function closeAgent() { + clearTimeout(subTimer) view = null render() $("activity-close").focus() @@ -754,6 +870,7 @@ function closeAgent() { async function readAgent() { if (!chat || !view) return + if (view.kind === "sub") return readSub() const { kind, id } = view let r try { @@ -779,7 +896,14 @@ async function readAgent() { a.role ? h("p", { class: "agent__role", text: a.role }) : null, a.stoppable ? h("button", { type: "button", class: "btn btn--sm", text: t("Stop %(name)s", { name: a.name || id }), onclick: () => stopAgent(a) }) : null, ) - const turns = (r.transcript || []).map((turn) => { + const turns = turnsOf(r.transcript || []) + body.replaceChildren(head, h("div", { class: "convo" }, turns.length ? turns : h("p", { class: "activity__empty", text: t("Nothing yet.") }))) + if (atEnd) body.scrollTop = body.scrollHeight +} + +/** A conversation's turns, read-only: the person's (or the harness's) messages and the replies. */ +function turnsOf(list) { + return list.map((turn) => { if (turn.role === "user") { return h("article", { class: `msg msg--user convo__turn${turn.machine ? " is-machine" : ""}` }, h("div", { class: "msg__text", text: String(turn.text || "") })) } @@ -806,8 +930,6 @@ async function readAgent() { turn.running ? h("p", { class: "msg__status muted", text: t("Writing…") }) : null, ) }) - body.replaceChildren(head, h("div", { class: "convo" }, turns.length ? turns : h("p", { class: "activity__empty", text: t("Nothing yet.") }))) - if (atEnd) body.scrollTop = body.scrollHeight } // --- Start ------------------------------------------------------------------------------------------ diff --git a/ui/js/chat.js b/ui/js/chat.js index fc9da96..e924e89 100644 --- a/ui/js/chat.js +++ b/ui/js/chat.js @@ -13,9 +13,9 @@ import { markdown } from "./markdown.js" import { openViewer, showKind } from "./viewer.js" import { readLocal } from "./explorer.js" import { menu, ask, showLine, popover, closePopover } from "./popup.js" -import { avatar, agentsControl, teamPossible, namedOnly, subagentsCap, SUBAGENTS_HINT, SUBAGENTS_OWN, levelsOf, levelWord, picture } from "./models.js" +import { avatar, agentsControl, teamPossible, namedOnly, subagentsCap, noteSubagentsCap, SUBAGENTS_HINT, SUBAGENTS_OWN, levelsOf, levelWord, picture } from "./models.js" import { readyIds, sending, sent, discard } from "./attach.js" -import { watchActivity, activityChanged, planList, activityButton } from "./activity.js" +import { watchActivity, activityChanged, planList, activityButton, treeChanged } from "./activity.js" import { agentBlock, agentReady, agentState, onAgentChange } from "./agent.js" import { folderName } from "./list.js" import { isProgress, progressLine, progressTicks } from "./progress.js" @@ -66,6 +66,38 @@ function terminalTarget() { return { chat: chat.id, device: device.id, directory: chat.device.directory, name: device.name || t("Terminal") } } +// A new agent chat's subagent facts (spec 3.4.0 §14): `/models?kind=agent&device=` -- each model's +// `subagents` for an agent chat, and `agent_subagents { offered, device_runs }` once. Read per +// device, again when a new chat starts; a server before 3.4.0 says nothing (no setting, as before). +const agentFacts = new Map() // device -> { at, models: Map(id -> subagents), offered, runs } | null + +async function readAgentFacts(device) { + if (!device) return null + const known = agentFacts.get(device) + if (known && Date.now() - known.at < 5000) return known + try { + const r = await get(`/models?kind=agent&device=${encodeURIComponent(device)}`) + if (!r.agent_subagents) { + agentFacts.set(device, { at: Date.now(), none: true }) + return agentFacts.get(device) + } + noteSubagentsCap(r.subagents_cap) + const facts = { at: Date.now(), models: new Map((r.models || []).map((m) => [m.id, m.subagents])), offered: !!r.agent_subagents.offered, runs: r.agent_subagents.device_runs } + agentFacts.set(device, facts) + return facts + } catch { + return null + } +} + +/** Whether a new agent chat on `model` is offered subagents: `{ cap, own }`, or null. */ +function agentOffer(facts, model) { + if (!facts || facts.none || !facts.offered || facts.runs === false) return null + const said = facts.models.get(model?.id) + if (said !== undefined && said !== null && !(typeof said === "object" ? said.offered : said)) return null + return { cap: subagentsCap(), own: namedOnly(said) } +} + /** Text into the message box, after what is there, for the person to read before sending. */ export function intoComposer(text) { const box = $("composer-text") @@ -412,6 +444,10 @@ class Live { case "ask": renderCards(this.chat, data.cards || []) break + case "tree": + // The subagent tree as it changed (spec 3.4.0): the activity panel draws it. + treeChanged(this.chat.id, data.tree) + break case "jobs": this.jobs.hidden = !(data.jobs || []).length append( @@ -522,6 +558,11 @@ function cardOf(chat, card) { const part = h("div", { class: "ask__item" }) // A background subagent asks, not the conversation's own turn: said first (`job`). if (item.job) part.append(h("p", { class: "ask__header", text: t("%(job)s asks:", { job: item.job }) })) + // A subagent of the tree asks, for a call nobody in the tree may allow (spec 3.4.0, §7). + if (item.subagent) { + const sub = item.subagent + part.append(h("p", { class: "ask__header ask__subagent", text: t("Subagent %(name)s (%(id)s), %(position)s, asks:", { name: sub.name || sub.id, id: sub.id, position: sub.position || "—" }) })) + } if (item.header) part.append(h("p", { class: "ask__header", text: item.header })) let detailShown = false if (item.kind === "approval") { @@ -822,6 +863,16 @@ async function renderChips() { }, mayPick)) if (model) parts.push(effortChip(model, draft.effort, (l) => ((draft.effort = l), renderChips()))) if (draft.kind === "chat" && teamPossible(model)) parts.push(agentsChip(draft.agents, (a) => ((draft.agents = a), renderChips()), { cap: subagentsCap(), own: namedOnly(model?.subagents) })) + // An agent chat's subagents run on its device: offered as the server says for that device. + const device = draft.kind === "agent" ? draftDevice() : "" + const known = device ? agentFacts.get(device) : null + const offer = device ? agentOffer(known, model) : null + if (offer) parts.push(agentsChip(draft.agents, (a) => ((draft.agents = a), renderChips()), offer)) + if (device && (!known || Date.now() - known.at > 5000)) { + readAgentFacts(device).then((f) => { + if (f && drawing === chipsDrawn && draft?.kind === "agent") renderChips() + }) + } // Last, so the chat's own settings come first where the line scrolls. if (shell) { const b = chip({ ico: ICONS.terminal, label: t("Terminal"), menuish: false, cls: "cchip--terminal", onclick: toggleTerminal }) diff --git a/ui/js/progress.js b/ui/js/progress.js index 6d4b18a..c2dcc16 100644 --- a/ui/js/progress.js +++ b/ui/js/progress.js @@ -4,29 +4,29 @@ // sentences. The server sends the object beside the old `status` text in a chat's stream; where it // sends none (an older server), the status text is drawn as before. // -// The texts are the spec's, with its `{{field}}` written as the page's `%(field)s`; the server's -// catalogues translate them under the same English (scripts/gen-i18n.py). +// The texts are the spec's, word for word with their `{{field}}`s: the server's catalogues +// translate them under the same keys (scripts/gen-i18n.py takes them from there). /** The thirteen phases: a text for each, and a variant where a field may be missing. */ const TEXTS = { queued: "Queued", - "queued.ahead": "Queued: %(ahead)s ahead", - waiting_for_model: "Waiting for %(model)s: %(reason)s", - loading_model: "Loading %(model)s", - reading_prompt: "Reading the prompt: %(done)s / %(total)s tokens (%(percent)s%%)", + "queued.ahead": "Queued: {{ahead}} ahead", + waiting_for_model: "Waiting for {{model}}: {{reason}}", + loading_model: "Loading {{model}}", + reading_prompt: "Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)", "reading_prompt.bare": "Reading the prompt", thinking: "Thinking", - writing: "Writing: %(rate)s tok/s", + writing: "Writing: {{rate}} tok/s", "writing.bare": "Writing", - running_tool: "Running %(tool)s: %(what)s", - "running_tool.bare": "Running %(tool)s", - waiting_for_person: "Waiting for you: %(what)s", - waiting_for_subagents: "Waiting for subagents: %(running)s running", + running_tool: "Running {{tool}}: {{what}}", + "running_tool.bare": "Running {{tool}}", + waiting_for_person: "Waiting for you: {{what}}", + waiting_for_subagents: "Waiting for subagents: {{running}} running", compacting: "Compacting the conversation", - retrying: "Retrying in %(in)s s: %(reason)s", + retrying: "Retrying in {{in}} s: {{reason}}", stopped: "Stopped", - "stopped.reason": "Stopped: %(reason)s", - failed: "Failed: %(reason)s", + "stopped.reason": "Stopped: {{reason}}", + failed: "Failed: {{reason}}", } /** The words a field stands for. */ @@ -36,7 +36,7 @@ const REASONS = { group: "its server holds another model now", } const WHAT = { - approval: "allow %(tool)s?", + approval: "allow {{tool}}?", "approval.bare": "an approval", question: "a question", plan: "the plan", @@ -44,19 +44,17 @@ const WHAT = { const STOPPED = { person: "by you", steps: "step limit", budget: "budget" } /** Every sentence above, for scripts/gen-i18n.py to find (it reads `t("…")` calls by hand). */ -export const PROGRESS_SENTENCES = [...Object.values(TEXTS), ...Object.values(REASONS), ...Object.values(WHAT), ...Object.values(STOPPED), "%(text)s · %(elapsed)s"] +export const PROGRESS_SENTENCES = [...Object.values(TEXTS), ...Object.values(REASONS), ...Object.values(WHAT), ...Object.values(STOPPED)] const NO_ELAPSED = ["stopped", "failed"] const has = (p, k) => p[k] !== undefined && p[k] !== null && p[k] !== "" -/** A sentence in the page's language, `%%` a percent sign (the server's way), then its - * `%(field)s` filled -- in that order, so a field's own text is never read as a template. */ +/** A sentence in the page's language (the spec's text is the key, as in the server's + * catalogues), its `{{field}}`s filled -- after the lookup, so a field's own text is never read + * as a template. */ function say(text, fields = {}) { - return t(text) - .replace(/%%/g, "\u0000") - .replace(/%\((\w+)\)s/g, (all, name) => (Object.prototype.hasOwnProperty.call(fields, name) ? String(fields[name]) : all)) - .replace(/\u0000/g, "%") + return t(text).replace(/\{\{(\w+)\}\}/g, (all, name) => (Object.prototype.hasOwnProperty.call(fields, name) ? String(fields[name]) : all)) } /** An integer as it is; a rate with one decimal, in the page's language (12.5, 12,5). */ @@ -126,7 +124,8 @@ export function progressText(p, now = Date.now()) { export function progressLine(p, now = Date.now(), { bare = false } = {}) { const text = progressText(p, now) if (!text || bare || NO_ELAPSED.includes(p.phase) || !has(p, "since")) return text - return say("%(text)s · %(elapsed)s", { text, elapsed: elapsed(now - Number(p.since)) }) + // progress.line, `{{text}} · {{elapsed}}`: the same in every language. + return `${text} · ${elapsed(now - Number(p.since))}` } /** Whether the line changes by itself as time passes (it has a clock, or a countdown). */ diff --git a/ui/js/tree.js b/ui/js/tree.js index 58e4174..1b4368b 100644 --- a/ui/js/tree.js +++ b/ui/js/tree.js @@ -1,24 +1,25 @@ -// The subagent tree (harness spec 3.4.0, subagents.md §12) as the activity panel draws it: the -// main agent at the root, every subagent under its parent in the order added, the open slots, -// then the ended ones, dimmed, with how each ended. Per agent: its name and id, its position, -// its state, model, mode (and request options), how full its context is, its current phase (the -// progress line), its messages in the room, and -- for any subagent, live or ended -- its session -// read-only and its findings; a live one can be stopped. +// The subagent tree (harness spec 3.4.0, subagents.md §12) as the activity panel draws it, the +// server's own panel in this app (`chat/_activity_tasks.html` tree_row): the main agent at the +// root, every subagent under its parent in the order added, how many slots are open, then the +// ended ones folded apart, dimmed, with how each ended. A row is the tasks' grid -- chevron, who +// it is and what it is doing, its context use in the time column, a stop square -- and opened, +// its facts (state, model, mode and request options, messages, findings), its task, its phase +// line, the last room message to or from it, and the buttons to read its conversation and its +// findings. Its subagents nest under it. // -// The data is the spec's, as the server passes it on in /api/v2 for a chat's own subagents and -// for an agent chat's alike: +// The data is the server's `GET /api/v2/chats/{chat}/tree` (and the stream's `tree` frame): // // { min, max, size, open_slots, // agents: [{ id, name, position, parent, state, model, mode, request, manager, task, // context: { used, window }, progress, findings, messages }], -// ended: [{ id, name, position, parent, state, ended, findings }] } +// ended: [{ ..., ended }] } // // Everything in it but the numbers is a model's words: set as text, never as markup. import { h, icon, ICONS } from "./dom.js" -import { progressLine } from "./progress.js" +import { progressLine, progressText } from "./progress.js" -/** The states' words (loop.json team.read.states). */ +/** The states' words (loop.json team.read.states; the server's TREE_STATE_WORDS). */ const STATES = { working: "working", waiting: "waiting for a slot", @@ -29,7 +30,8 @@ const STATES = { out_of_context: "out of context", stopped: "stopped", } -export const TREE_SENTENCES = Object.values(STATES) +/** A subagent that may still be stopped (the server's TREE_LIVE). */ +export const TREE_LIVE = ["working", "waiting", "paused"] export function treeState(state) { return STATES[state] ? t(STATES[state]) : String(state || "") @@ -43,20 +45,19 @@ export function contextPercent(c) { return Math.min(100, Math.floor((used * 100) / window)) } -function modeWords(a) { - if (!a.mode) return "" - const req = (a.request || []).filter(Boolean) - return req.length ? `${a.mode} ${req.map((r) => `+ ${r}`).join(" ")}` : a.mode -} - /** The agents in tree order (an agent, then those under it, depth first), each with its depth. */ export function treeOrder(agents) { - const ids = new Set(agents.map((a) => a.id)) + const names = new Map() + for (const a of agents) names.set(a.id, a.id).set(a.name, a.id) + const parentOf = (a) => { + const p = a.parent ? names.get(a.parent) : null + return p && p !== a.id ? p : null + } const under = new Map() for (const a of agents) { - const parent = a.parent && ids.has(a.parent) && a.parent !== a.id ? a.parent : null - if (!under.has(parent)) under.set(parent, []) - under.get(parent).push(a) + const p = parentOf(a) + if (!under.has(p)) under.set(p, []) + under.get(p).push(a) } const out = [] const seen = new Set() @@ -73,7 +74,7 @@ export function treeOrder(agents) { return out } -/** The room's messages to or from an agent (by name; `all` reaches everyone). */ +/** The room's messages to or from an agent (by name). */ function messagesOf(room, a) { return room.filter((m) => m.from === a.name || (m.to || []).includes(a.name) || (m.recipients || []).includes(a.name)) } @@ -83,33 +84,25 @@ function firstLine(text) { } /** - * The tree as rows. `act` holds what the rows can do: `open(a)` (its session, read-only), + * The tree as rows. `act` holds what the rows can do: `open(a)` (its conversation, read-only), * `findings(a)`, `stop(a)`; `isOpen(key)` / `toggled(key, open)` keep unfolded rows unfolded - * across redraws; `room` is the room as read so far. + * across redraws. `room` is the room as read so far; `device` says the subagents run on the + * computer the chat runs on (an agent chat). */ -export function treePart(tree, { room = [], act = {}, now = Date.now() } = {}) { - const rows = [] - for (const { agent, depth } of treeOrder(tree.agents || [])) rows.push(agentRow(agent, depth, { room, act, now, ended: false })) - for (let i = 0; i < (Number(tree.open_slots) || 0); i++) { - rows.push(h("div", { class: "subagent subagent--slot", style: `--depth: 1` }, h("span", { class: "subagent__slot", text: t("Open slot") }))) - } - const ended = tree.ended || [] +export function treePart(tree, { room = [], act = {}, now = Date.now(), device = false } = {}) { const box = h("div", { class: "subagents" }) - box.append( - h("p", { class: "subagents__size", text: t("%(size)s of at most %(max)s agents", { size: tree.size ?? (tree.agents || []).length, max: tree.max ?? "?" }) }), - h("div", { class: "subagents__list" }, rows), - ) + box.append(h("p", { class: "tasks__team", text: t("%(size)s of %(max)s", { size: tree.size ?? (tree.agents || []).length, max: tree.max ?? "?" }) })) + if (Number(tree.open_slots) > 0) box.append(h("p", { class: "tasks__goal subagents__slots", text: t("Open slots: %(n)s", { n: tree.open_slots }) })) + if (device) box.append(h("p", { class: "tasks__goal", text: t("They run on the computer this chat runs on.") })) + box.append(h("div", { class: "tasks__list subagents__list" }, treeOrder(tree.agents || []).map(({ agent, depth }) => row(agent, depth, { room, act, now, ended: false })))) + const ended = tree.ended || [] if (ended.length) { const key = "fold:ended" const fold = h( "details", - { class: "tasks__more subagents__ended", open: act.isOpen?.(key) }, - h("summary", null, h("span", { class: "tasks__chev", "aria-hidden": "true" }, icon(ICONS.chevron)), h("span", { text: t("Ended %(n)s", { n: ended.length }) })), - h( - "div", - { class: "subagents__list" }, - ended.map((a) => agentRow(a, 0, { room, act, now, ended: true })), - ), + { class: "tasks__more subagents__ended", open: !!act.isOpen?.(key) }, + h("summary", null, h("span", { class: "tasks__chev", "aria-hidden": "true" }, icon(ICONS.chevron)), h("span", { text: `${t("Ended")} ${ended.length}` })), + h("div", { class: "tasks__list" }, ended.map((a) => row(a, 0, { room, act, now, ended: true }))), ) fold.addEventListener("toggle", () => act.toggled?.(key, fold.open)) box.append(fold) @@ -117,15 +110,17 @@ export function treePart(tree, { room = [], act = {}, now = Date.now() } = {}) { return box } -function agentRow(a, depth, { room, act, now, ended }) { +function row(a, depth, { room, act, now, ended }) { const key = `sub:${a.id}` - const main = !a.parent && a.id === "lead" - const pct = contextPercent(a.context) - const line = a.progress ? progressLine(a.progress, now) : "" + const main = !a.parent && (a.id === "lead" || a.name === "lead") + const pct = ended ? null : contextPercent(a.context) const said = messagesOf(room, a) const last = said[said.length - 1] + const mode = a.mode ? (a.request?.length ? `${a.mode} + ${a.request.join(", ")}` : a.mode) : "" + const live = !ended && TREE_LIVE.includes(a.state) + const phase = !ended && a.progress ? progressText(a.progress, now) : "" const stop = - !ended && !main && act.stop + live && !main && act.stop ? h( "button", { @@ -144,19 +139,25 @@ function agentRow(a, depth, { room, act, now, ended }) { : h("span", { class: "agent__stop is-none", "aria-hidden": "true" }) const el = h( "details", - { class: `agent subagent is-${String(a.state || "").replace(/[^a-z_]/g, "")}${ended ? " is-ended" : ""}`, open: act.isOpen?.(key), dataset: { id: a.id } }, + { class: `agent subagent is-${String(a.state || "").replace(/[^a-z_]/g, "")}${ended ? " is-ended" : ""}`, open: !!act.isOpen?.(key), dataset: { id: a.id } }, h( "summary", { class: "agent__row" }, h("span", { class: "agent__chev", "aria-hidden": "true" }, icon(ICONS.chevron)), h( "span", - { class: "agent__what", title: a.position || a.name }, - h("bdi", { class: "agent__name", text: a.name || a.id }), - h("span", { class: "subagent__id", text: ` (${a.id})` }), - a.position ? h("span", { class: "subagent__position", text: ` — ${main ? t("the main agent") : a.position}` }) : null, + { class: "agent__what" }, + h( + "span", + { class: "subagent__who" }, + h("bdi", { class: "agent__name", text: a.name || a.id }), + a.id !== a.name ? h("span", { class: "agent__id", text: a.id }) : null, + ), + main ? h("span", { class: "agent__role", text: t("the main agent") }) : a.position ? h("span", { class: "agent__role", text: a.position }) : null, + // The phase alone where a row has no room for its clock (progress.md §3); ended: how. + h("span", { class: "subagent__phase", dataset: { phase: phase ? a.progress.phase : "" }, text: phase || treeState(a.state) }), ), - h("span", { class: "subagent__pct", text: pct === null ? "" : `${pct}%`, title: pct === null ? "" : t("%(percent)s%% of the context window used", { percent: pct }).replace(/%%/g, "%") }), + h("span", { class: "agent__time", title: t("Context used"), text: pct === null ? "" : `${pct}%` }), stop, ), h( @@ -167,21 +168,19 @@ function agentRow(a, depth, { room, act, now, ended }) { { class: "agent__facts" }, h("span", { class: "agent__state", text: treeState(a.state) }), a.model ? h("span", { class: "agent__model", text: a.model }) : null, - modeWords(a) ? h("span", { class: "subagent__mode", text: modeWords(a) }) : null, - a.findings ? h("span", { text: t("Findings: %(n)s", { n: a.findings }) }) : null, + mode ? h("span", { text: mode }) : null, + h("span", { text: t("messages: %(n)s", { n: a.messages ?? said.length }) }), + a.findings ? h("span", { text: t("findings: %(n)s", { n: a.findings }) }) : null, ), - pct === null ? null : h("div", { class: "subagent__bar", role: "meter", "aria-valuemin": "0", "aria-valuemax": "100", "aria-valuenow": String(pct), "aria-label": t("Context") }, h("span", { style: `--pct: ${pct}%` })), - line ? h("p", { class: "subagent__phase", "data-phase": a.progress?.phase || "", text: line }) : null, - a.task ? h("p", { class: "agent__brief", text: a.task }) : null, - said.length || a.messages - ? h("p", { class: "subagent__room" }, h("span", { class: "chip", text: t("Messages: %(n)s", { n: a.messages ?? said.length }) }), last ? h("span", { class: "subagent__last", text: `${last.from}: ${firstLine(last.text)}` }) : null) - : null, + a.task && !main ? h("p", { class: "agent__brief", text: a.task }) : null, + phase ? h("p", { class: "agent__text subagent__line", dataset: { progress: JSON.stringify(a.progress) }, text: progressLine(a.progress, now) }) : null, + last ? h("p", { class: "agent__text", text: `${last.from} → ${(last.to || []).join(", ")}: ${firstLine(last.text)}` }) : null, main ? null : h( - "div", - { class: "subagent__actions" }, - act.open ? h("button", { type: "button", class: "btn btn--sm", text: t("Open its session"), onclick: () => act.open(a) }) : null, + "p", + { class: "subagent__buttons" }, + act.open ? h("button", { type: "button", class: "btn btn--sm", text: t("Open its conversation"), onclick: () => act.open(a) }) : null, act.findings ? h("button", { type: "button", class: "btn btn--sm", text: t("Findings"), onclick: () => act.findings(a) }) : null, ), ), @@ -190,3 +189,12 @@ function agentRow(a, depth, { room, act, now, ended }) { el.addEventListener("toggle", () => act.toggled?.(key, el.open)) return el } + +/** The tree's phase lines move on by themselves between reads (their clock). */ +export function tickTree(root, now = Date.now()) { + for (const el of root.querySelectorAll(".subagent__line[data-progress]")) { + try { + el.textContent = progressLine(JSON.parse(el.dataset.progress), now) + } catch {} + } +}