Terminal: a shell on the agent chat's computer, before the first message too
ci / check (push) Failing after 2s
ci / start (push) Failing after 2s

The Terminal panel opens a shell on the device an agent chat works on, in its
folder: for a new agent chat as soon as the device and folder are chosen (the
server's /api/v2/devices/{device}/terminal/ws?directory=, which the chat's first
message takes over -- the same socket carries on), and for an open agent chat
from the header (/api/v2/chats/{chat}/terminal/ws, the web page's shell).
Offered where GET /devices says the device opens a terminal.

The socket is Rust's (src-tauri/src/terminal.rs, tungstenite on the rustls
ureq already brings): the bearer token goes in the handshake and never reaches
the page, which gets the frames as `terminal` events and sends keys, sizes and
close through commands. 4401 signs the app out as a 401 does.

xterm and its fit addon are the server's vendored build, pinned through
fetch-vendor.sh like CodeMirror and loaded on the first open. Send to chat puts
the selection, the last marked command and its output, or the screen into the
composer, never sent. The terminal and the activity panel take turns in the
panel column. Continue in a terminal stays in the chat's menu.

tests/ui: seven B3 cases with a mocked socket, at 1280 and 390 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-10 09:06:01 +00:00
1 parent ec32bca644
commit ac0d4ce623
21 files changed
+1989 -24

No files matched your search

+10
View File
@@ -24,6 +24,16 @@ All notable changes to LLeMbas Desktop. The format follows
### Added ### Added
- **Terminal.** An agent chat has a Terminal panel: a shell on the computer the chat works on, in
its folder, beside the chat (over it in a narrow window). A new agent chat offers it as soon as
the folder is chosen, before the first message -- and the chat that message makes carries on in
the same shell. An open agent chat opens its own from the header, the same shell the web
interface shows. **Send to chat** puts the selection, or the last command and its output, into
the message box to read before sending; the panel copies them too, and steps between commands
where the shell marks them. The terminal and the activity panel take turns beside the chat;
closing either keeps the shell. Offered where the computer allows a terminal (needs the server's
2.0.0-beta.8). The app opens the connection itself, with its sign-in: the page never holds it.
"Continue in a terminal" is now in the chat's menu only.
- `tests/ui/`: the page under a headless browser against a stand-in for the app's bridge, with - `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). the server's answers as a table (`node tests/ui/run.mjs`, needs Playwright and a Chromium).
+16 -4
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Put the server's vendored CodeMirror build where the app's own pages read it from: # Put the server's vendored CodeMirror build and xterm where the app's own pages read them from:
# ui/vendor/*.mjs (copies of the server's static/vendor/cm-*.mjs and lezer-*.mjs) # ui/vendor/*.mjs (copies of the server's static/vendor/cm-*.mjs and lezer-*.mjs)
# ui/vendor/xterm.js, xterm.css, xterm-addon-fit.js (the terminal panel's, copied unchanged:
# classic scripts the panel loads when it is first opened)
# #
# scripts/fetch-vendor.sh <server-ref> copy the modules at that ref (a tag such as # scripts/fetch-vendor.sh <server-ref> copy the modules at that ref (a tag such as
# v2.0.0-beta.1, or a branch) and write vendor.lock.json # v2.0.0-beta.1, or a branch) and write vendor.lock.json
@@ -47,8 +49,8 @@ if [ $# -eq 0 ]; then
echo "fetch-vendor: $OUT/$name does not match $LOCK (server $ref)" >&2 echo "fetch-vendor: $OUT/$name does not match $LOCK (server $ref)" >&2
bad=1 bad=1
fi fi
done < <(sed -n -E 's/^ *"([a-z0-9-]+\.mjs)": *\{"server": *"[0-9a-f]{64}", *"copy": *"([0-9a-f]{64})"\}.*/\1\t\2/p' "$LOCK") done < <(sed -n -E 's/^ *"([a-z0-9-]+\.(mjs|js|css))": *\{"server": *"[0-9a-f]{64}", *"copy": *"([0-9a-f]{64})"\}.*/\1\t\3/p' "$LOCK")
for f in "$OUT"/*.mjs; do for f in "$OUT"/*; do
grep -q "\"$(basename "$f")\"" "$LOCK" || { echo "fetch-vendor: $f is not in $LOCK" >&2; bad=1; } grep -q "\"$(basename "$f")\"" "$LOCK" || { echo "fetch-vendor: $f is not in $LOCK" >&2; bad=1; }
done done
[ "$count" -gt 0 ] || { echo "fetch-vendor: $LOCK lists no file" >&2; exit 1; } [ "$count" -gt 0 ] || { echo "fetch-vendor: $LOCK lists no file" >&2; exit 1; }
@@ -87,6 +89,16 @@ for name in $names; do
fi fi
entries+=("$(printf ' "%s": {"server": "%s", "copy": "%s"}' "$name" "$have" "$(sha "$work/out/$name")")") entries+=("$(printf ' "%s": {"server": "%s", "copy": "%s"}' "$name" "$have" "$(sha "$work/out/$name")")")
done done
# xterm and its fit addon, for the terminal panel: copied as they are, each checked against the
# server's pin like the modules.
for name in xterm.js xterm.css xterm-addon-fit.js; do
git -C "$SERVER" show "$commit:$SOURCE/$name" > "$work/out/$name" \
|| { echo "fetch-vendor: $name is not in $SOURCE at $REF" >&2; exit 1; }
pinned="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2], {}).get("sha256", ""))' "$work/server-lock.json" "$name")"
have="$(sha "$work/out/$name")"
[ "$pinned" = "$have" ] || { echo "fetch-vendor: $name does not match the server's pin ($have, pinned $pinned)" >&2; exit 1; }
entries+=("$(printf ' "%s": {"server": "%s", "copy": "%s"}' "$name" "$have" "$have")")
done
# The polyfill stub, from the server's own scripts (it is not a download, so it has no pin there). # The polyfill stub, from the server's own scripts (it is not a download, so it has no pin there).
stub="src/lembas/web/static/js/node-process.mjs" stub="src/lembas/web/static/js/node-process.mjs"
git -C "$SERVER" show "$commit:$stub" > "$work/out/node-process.mjs" \ git -C "$SERVER" show "$commit:$stub" > "$work/out/node-process.mjs" \
@@ -101,7 +113,7 @@ done
rm -rf "$OUT" rm -rf "$OUT"
mkdir -p "$OUT" mkdir -p "$OUT"
cp "$work"/out/*.mjs "$OUT/" cp "$work"/out/*.mjs "$work"/out/xterm.js "$work"/out/xterm.css "$work"/out/xterm-addon-fit.js "$OUT/"
{ {
printf '{\n "tag": "%s",\n "commit": "%s",\n "files": {\n' "$REF" "$commit" printf '{\n "tag": "%s",\n "commit": "%s",\n "files": {\n' "$REF" "$commit"
last=$((${#entries[@]} - 1)) last=$((${#entries[@]} - 1))
+39
View File
@@ -376,6 +376,29 @@ KEYS = (
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.", "Nothing runs in the background. Subagents, a team's members and background commands show here while they work.",
"Team: %(size)s of at most %(max)s subagents", "Team: %(size)s of at most %(max)s subagents",
"How many work on it at once, the run's own agent among them, on the same model.", "How many work on it at once, the run's own agent among them, on the same model.",
# terminal.js
"Terminal",
"Close terminal",
"Send to chat",
"Connecting…",
"Connected.",
"Connected. This shell is also open in another tab, and they share a size.",
"Disconnected. Close and reopen to reconnect.",
"This terminal closed.",
"Reconnecting…",
"The terminal could not be loaded.",
"Nothing has been run in this shell yet.",
"Nothing to send: select some output first.",
"Copied the last of the screen, as it appeared. This shell does not mark where commands begin.",
"Copied.",
"The previous command",
"The previous command (Ctrl+Shift+↑)",
"The next command",
"The next command (Ctrl+Shift+↓)",
"Copy the last command and its output",
"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 app's own sentences. The mode names (Manual, Edit, Auto, Plan) stay English, as the # The app's own sentences. The mode names (Manual, Edit, Auto, Plan) stay English, as the
@@ -383,6 +406,8 @@ KEYS = (
# the device, as the server's catalogue has it. # the device, as the server's catalogue has it.
OWN: dict[str, dict[str, str]] = { OWN: dict[str, dict[str, str]] = {
"cs": { "cs": {
"Sign in again: the server no longer accepts this app's sign-in.": "Přihlaste se znovu: server už nepřijímá přihlášení této aplikace.",
"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.",
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Tento model vyřizuje najednou jeden požadavek: podagenti potřebují jiný model na jiném serveru, pojmenovaný v konverzaci.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Tento model vyřizuje najednou jeden požadavek: podagenti potřebují jiný model na jiném serveru, pojmenovaný v konverzaci.",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Čísla počítají i vlastního agenta konverzace: 1 je agent sám, bez podagentů; Max 4 je agent a až 3 podagenti. Podagent běží na modelu konverzace, na jiném jen tehdy, když ho v konverzaci pojmenujete.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Čísla počítají i vlastního agenta konverzace: 1 je agent sám, bez podagentů; Max 4 je agent a až 3 podagenti. Podagent běží na modelu konverzace, na jiném jen tehdy, když ho v konverzaci pojmenujete.",
"Min": "Min", "Min": "Min",
@@ -604,6 +629,8 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Žádný den nezaškrtnutý: každý den.", "No day ticked: every day.": "Žádný den nezaškrtnutý: každý den.",
}, },
"de": { "de": {
"Sign in again: the server no longer accepts this app's sign-in.": "Melden Sie sich erneut an: Der Server nimmt die Anmeldung dieser App nicht mehr an.",
"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.",
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Dieses Modell bedient eine Anfrage zur Zeit: Subagenten brauchen ein anderes Modell auf einem anderen Server, im Chat genannt.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Dieses Modell bedient eine Anfrage zur Zeit: Subagenten brauchen ein anderes Modell auf einem anderen Server, im Chat genannt.",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Die Zahlen zählen den eigenen Agenten des Chats mit: 1 ist der Agent allein, ohne Subagenten; Max 4 ist der Agent und bis zu 3 Subagenten. Ein Subagent läuft auf dem Modell des Chats, auf einem anderen nur, wenn Sie es im Chat nennen.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Die Zahlen zählen den eigenen Agenten des Chats mit: 1 ist der Agent allein, ohne Subagenten; Max 4 ist der Agent und bis zu 3 Subagenten. Ein Subagent läuft auf dem Modell des Chats, auf einem anderen nur, wenn Sie es im Chat nennen.",
"Min": "Min", "Min": "Min",
@@ -825,6 +852,8 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Kein Tag angehakt: jeden Tag.", "No day ticked: every day.": "Kein Tag angehakt: jeden Tag.",
}, },
"es": { "es": {
"Sign in again: the server no longer accepts this app's sign-in.": "Vuelva a iniciar sesión: el servidor ya no acepta el inicio de sesión de esta aplicación.",
"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.",
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Este modelo atiende una petición a la vez: los subagentes necesitan otro modelo, en otro servidor, nombrado en el chat.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Este modelo atiende una petición a la vez: los subagentes necesitan otro modelo, en otro servidor, nombrado en el chat.",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Los números cuentan el agente propio del chat: 1 es el agente solo, sin subagentes; Máx. 4 es el agente y hasta 3 subagentes. Un subagente usa el modelo del chat, u otro solo si lo nombra en el chat.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Los números cuentan el agente propio del chat: 1 es el agente solo, sin subagentes; Máx. 4 es el agente y hasta 3 subagentes. Un subagente usa el modelo del chat, u otro solo si lo nombra en el chat.",
"Min": "Mín.", "Min": "Mín.",
@@ -1046,6 +1075,8 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Ningún día marcado: todos los días.", "No day ticked: every day.": "Ningún día marcado: todos los días.",
}, },
"fr": { "fr": {
"Sign in again: the server no longer accepts this app's sign-in.": "Reconnectez-vous : le serveur n’accepte plus la connexion de cette application.",
"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.",
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ce modèle traite une requête à la fois : les sous-agents ont besoin d’un autre modèle, sur un autre serveur, nommé dans la conversation.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ce modèle traite une requête à la fois : les sous-agents ont besoin d’un autre modèle, sur un autre serveur, nommé dans la conversation.",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Les nombres comptent l’agent de la conversation : 1, c’est l’agent seul, sans sous-agents ; Max 4, c’est l’agent et jusqu’à 3 sous-agents. Un sous-agent utilise le modèle de la conversation, ou un autre seulement si vous le nommez dans la conversation.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Les nombres comptent l’agent de la conversation : 1, c’est l’agent seul, sans sous-agents ; Max 4, c’est l’agent et jusqu’à 3 sous-agents. Un sous-agent utilise le modèle de la conversation, ou un autre seulement si vous le nommez dans la conversation.",
"Min": "Min", "Min": "Min",
@@ -1267,6 +1298,8 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Aucun jour coché : tous les jours.", "No day ticked: every day.": "Aucun jour coché : tous les jours.",
}, },
"hu": { "hu": {
"Sign in again: the server no longer accepts this app's sign-in.": "Jelentkezzen be újra: a szerver már nem fogadja el ennek az alkalmazásnak a bejelentkezését.",
"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.",
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ez a modell egyszerre egy kérést szolgál ki: az alügynököknek másik modell kell, másik szerveren, a beszélgetésben megnevezve.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ez a modell egyszerre egy kérést szolgál ki: az alügynököknek másik modell kell, másik szerveren, a beszélgetésben megnevezve.",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "A számokba a beszélgetés saját ügynöke is beleszámít: az 1 maga az ügynök, alügynökök nélkül; a Max. 4 az ügynök és legfeljebb 3 alügynök. Az alügynök a beszélgetés modelljén fut, másikon csak akkor, ha megnevezi a beszélgetésben.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "A számokba a beszélgetés saját ügynöke is beleszámít: az 1 maga az ügynök, alügynökök nélkül; a Max. 4 az ügynök és legfeljebb 3 alügynök. Az alügynök a beszélgetés modelljén fut, másikon csak akkor, ha megnevezi a beszélgetésben.",
"Min": "Min.", "Min": "Min.",
@@ -1488,6 +1521,8 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Ha egy nap sincs bejelölve: minden nap.", "No day ticked: every day.": "Ha egy nap sincs bejelölve: minden nap.",
}, },
"it": { "it": {
"Sign in again: the server no longer accepts this app's sign-in.": "Accedi di nuovo: il server non accetta più l’accesso di questa app.",
"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.",
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Questo modello serve una richiesta alla volta: i sottoagenti hanno bisogno di un altro modello, su un altro server, nominato nella chat.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Questo modello serve una richiesta alla volta: i sottoagenti hanno bisogno di un altro modello, su un altro server, nominato nella chat.",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "I numeri contano anche l’agente della chat: 1 è l’agente da solo, senza sottoagenti; Max 4 è l’agente più fino a 3 sottoagenti. Un sottoagente usa il modello della chat, o un altro solo se lo nomini nella chat.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "I numeri contano anche l’agente della chat: 1 è l’agente da solo, senza sottoagenti; Max 4 è l’agente più fino a 3 sottoagenti. Un sottoagente usa il modello della chat, o un altro solo se lo nomini nella chat.",
"Min": "Min", "Min": "Min",
@@ -1709,6 +1744,8 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Nessun giorno spuntato: ogni giorno.", "No day ticked: every day.": "Nessun giorno spuntato: ogni giorno.",
}, },
"pl": { "pl": {
"Sign in again: the server no longer accepts this app's sign-in.": "Zaloguj się ponownie: serwer nie przyjmuje już logowania tej aplikacji.",
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Terminal wymaga nowszego serwera LLeMbas. Poproś jego administratora o aktualizację.",
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ten model obsługuje jedno żądanie naraz: podagenci potrzebują innego modelu, na innym serwerze, wskazanego w czacie.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ten model obsługuje jedno żądanie naraz: podagenci potrzebują innego modelu, na innym serwerze, wskazanego w czacie.",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Liczby obejmują własnego agenta czatu: 1 to sam agent, bez podagentów; Maks 4 to agent i do 3 podagentów. Podagent działa na modelu czatu, na innym tylko wtedy, gdy wskażesz go w czacie.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Liczby obejmują własnego agenta czatu: 1 to sam agent, bez podagentów; Maks 4 to agent i do 3 podagentów. Podagent działa na modelu czatu, na innym tylko wtedy, gdy wskażesz go w czacie.",
"Min": "Min", "Min": "Min",
@@ -1930,6 +1967,8 @@ OWN: dict[str, dict[str, str]] = {
"No day ticked: every day.": "Żaden dzień niezaznaczony: codziennie.", "No day ticked: every day.": "Żaden dzień niezaznaczony: codziennie.",
}, },
"sk": { "sk": {
"Sign in again: the server no longer accepts this app's sign-in.": "Prihláste sa znova: server už neprijíma prihlásenie tejto aplikácie.",
"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.",
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Tento model vybavuje naraz jednu požiadavku: podagenti potrebujú iný model na inom serveri, pomenovaný v konverzácii.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Tento model vybavuje naraz jednu požiadavku: podagenti potrebujú iný model na inom serveri, pomenovaný v konverzácii.",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Čísla počítajú aj vlastného agenta konverzácie: 1 je agent sám, bez podagentov; Max 4 je agent a až 3 podagenti. Podagent beží na modeli konverzácie, na inom len vtedy, keď ho v konverzácii pomenujete.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Čísla počítajú aj vlastného agenta konverzácie: 1 je agent sám, bez podagentov; Max 4 je agent a až 3 podagenti. Podagent beží na modeli konverzácie, na inom len vtedy, keď ho v konverzácii pomenujete.",
"Min": "Min", "Min": "Min",
+84 -3
View File
@@ -279,6 +279,15 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "block-buffer"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"hybrid-array",
]
[[package]] [[package]]
name = "block2" name = "block2"
version = "0.6.2" version = "0.6.2"
@@ -511,6 +520,12 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]] [[package]]
name = "cookie" name = "cookie"
version = "0.18.2" version = "0.18.2"
@@ -623,6 +638,15 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "crypto-common"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"hybrid-array",
]
[[package]] [[package]]
name = "cssparser" name = "cssparser"
version = "0.37.0" version = "0.37.0"
@@ -686,6 +710,12 @@ dependencies = [
"syn 3.0.8", "syn 3.0.8",
] ]
[[package]]
name = "data-encoding"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
[[package]] [[package]]
name = "dbus" name = "dbus"
version = "0.9.12" version = "0.9.12"
@@ -775,8 +805,19 @@ version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer 0.10.4",
"crypto-common", "crypto-common 0.1.7",
]
[[package]]
name = "digest"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer 0.12.1",
"const-oid",
"crypto-common 0.2.2",
] ]
[[package]] [[package]]
@@ -1552,6 +1593,15 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "hybrid-array"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
dependencies = [
"typenum",
]
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.12.0" version = "1.12.0"
@@ -2097,6 +2147,7 @@ dependencies = [
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-single-instance", "tauri-plugin-single-instance",
"tauri-plugin-updater", "tauri-plugin-updater",
"tungstenite",
"ureq", "ureq",
] ]
@@ -3488,6 +3539,17 @@ dependencies = [
"stable_deref_trait", "stable_deref_trait",
] ]
[[package]]
name = "sha1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
dependencies = [
"cfg-if",
"cpufeatures 0.3.1",
"digest 0.11.3",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.9" version = "0.10.9"
@@ -3496,7 +3558,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures 0.2.17", "cpufeatures 0.2.17",
"digest", "digest 0.10.7",
] ]
[[package]] [[package]]
@@ -4539,6 +4601,25 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22"
dependencies = [
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"rand",
"rustls",
"rustls-native-certs 0.8.5",
"rustls-pki-types",
"sha1",
"thiserror 2.0.21",
]
[[package]] [[package]]
name = "typeid" name = "typeid"
version = "1.0.3" version = "1.0.3"
+2
View File
@@ -26,6 +26,8 @@ tauri-plugin-updater = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
ureq = { version = "2", default-features = false, features = ["tls", "native-certs", "json"] } ureq = { version = "2", default-features = false, features = ["tls", "native-certs", "json"] }
# The terminal panel's socket (`terminal.rs`): a blocking WebSocket on the TLS ureq already brings.
tungstenite = { version = "0.30", default-features = false, features = ["handshake", "rustls-tls-native-roots"] }
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2" libc = "0.2"
+74
View File
@@ -22,6 +22,7 @@ mod remote;
mod sealed; mod sealed;
mod server; mod server;
mod settings; mod settings;
mod terminal;
mod watch; mod watch;
use std::collections::HashMap; use std::collections::HashMap;
@@ -115,6 +116,8 @@ struct App {
signing_in: Mutex<Option<Arc<AtomicBool>>>, signing_in: Mutex<Option<Arc<AtomicBool>>>,
streams: Mutex<HashMap<u64, Arc<AtomicBool>>>, streams: Mutex<HashMap<u64, Arc<AtomicBool>>>,
next_stream: AtomicU64, next_stream: AtomicU64,
/// The terminal panel's sockets (`terminal.rs`), by the page's id for each.
terminals: terminal::Sessions,
sealed: sealed::Pages, sealed: sealed::Pages,
watch: watch::Watch, watch: watch::Watch,
/// The files last dropped on the window: what `attach_dropped` may send, and nothing else. /// The files last dropped on the window: what `attach_dropped` may send, and nothing else.
@@ -271,6 +274,7 @@ async fn set_server(handle: AppHandle, address: String) -> Result<Status, String
if before.as_deref() != Some(d.base.as_str()) { if before.as_deref() != Some(d.base.as_str()) {
app.agent.stop(); app.agent.stop();
app.watch.reset(); app.watch.reset();
app.terminals.close_all();
} }
*app.account.lock().unwrap() = server::load_account(&app.config_dir, &d.base); *app.account.lock().unwrap() = server::load_account(&app.config_dir, &d.base);
*app.server.lock().unwrap() = Some(d); *app.server.lock().unwrap() = Some(d);
@@ -380,6 +384,7 @@ fn sign_out(handle: AppHandle) {
let account = app.account.lock().unwrap().take(); let account = app.account.lock().unwrap().take();
server::forget_account(&app.config_dir); server::forget_account(&app.config_dir);
app.watch.reset(); app.watch.reset();
app.terminals.close_all();
if let Some(a) = account { if let Some(a) = account {
std::thread::spawn(move || server::sign_out(&a.server, &a.token)); std::thread::spawn(move || server::sign_out(&a.server, &a.token));
} }
@@ -391,6 +396,7 @@ fn token_refused(handle: &AppHandle) {
let app = handle.state::<App>(); let app = handle.state::<App>();
if app.account.lock().unwrap().take().is_some() { if app.account.lock().unwrap().take().is_some() {
server::forget_account(&app.config_dir); server::forget_account(&app.config_dir);
app.terminals.close_all();
announce(handle); announce(handle);
} }
} }
@@ -511,6 +517,69 @@ fn stream_close(app: tauri::State<'_, App>, id: u64) {
} }
} }
// --- The terminal panel ------------------------------------------------------------------------
/// Open a shell on a linked device, as the terminal panel asks: an agent chat's (`chat`), or the
/// one before the chat exists (`device` and `directory`). The socket is opened here, with the
/// app's token in its handshake; the page gets its frames as `terminal` events with this id --
/// `output` (base64), `control` (the server's JSON), and a last `end`.
#[tauri::command]
fn terminal_open(
handle: AppHandle,
chat: Option<String>,
device: Option<String>,
directory: Option<String>,
cols: u16,
rows: u16,
) -> Result<u64, ApiError> {
let app = handle.state::<App>();
let Some((app_url, token)) = app.door() else {
return Err(ApiError::new(401, "Not signed in."));
};
let target = match (chat, device) {
(Some(chat), None) => terminal::Target::Chat(chat),
(None, Some(device)) => terminal::Target::Device {
device,
directory: directory.unwrap_or_default(),
},
_ => return Err(ApiError::new(400, "A chat or a device, not both.")),
};
let url = terminal::url(&app_url, &target, cols, rows)?;
let h = handle.clone();
Ok(app.terminals.open(url, token, move |down| {
if down.kind == "end" {
h.state::<App>().terminals.forget(down.id);
let refused = down.data.get("code").and_then(Value::as_u64)
== Some(u64::from(terminal::CLOSE_UNAUTHORIZED))
|| down.data.get("status").and_then(Value::as_u64) == Some(401);
if refused {
token_refused(&h);
}
}
let _ = h.emit("terminal", down);
}))
}
/// Keys typed (or pasted) into a terminal, as text: sent to the shell as its bytes.
#[tauri::command]
fn terminal_input(app: tauri::State<'_, App>, id: u64, data: String) -> bool {
app.terminals
.send(id, terminal::Up::Keys(data.into_bytes()))
}
/// The terminal's size changed.
#[tauri::command]
fn terminal_resize(app: tauri::State<'_, App>, id: u64, cols: u16, rows: u16) -> bool {
app.terminals.send(id, terminal::Up::Resize(cols, rows))
}
/// The page let go of a terminal: its socket is closed (the shell on the device lives on, as
/// the server keeps it).
#[tauri::command]
fn terminal_close(app: tauri::State<'_, App>, id: u64) {
app.terminals.close(id);
}
// --- Attachments ---------------------------------------------------------------------------------- // --- Attachments ----------------------------------------------------------------------------------
/// What became of one file sent as an attachment: the server's attachment, or why not. `reason` /// What became of one file sent as an attachment: the server's attachment, or why not. `reason`
@@ -1560,6 +1629,10 @@ pub fn run() {
terminal_word, terminal_word,
stream_open, stream_open,
stream_close, stream_close,
terminal_open,
terminal_input,
terminal_resize,
terminal_close,
local_list, local_list,
local_read, local_read,
sealed_open, sealed_open,
@@ -1621,6 +1694,7 @@ pub fn run() {
signing_in: Mutex::new(None), signing_in: Mutex::new(None),
streams: Mutex::new(HashMap::new()), streams: Mutex::new(HashMap::new()),
next_stream: AtomicU64::new(0), next_stream: AtomicU64::new(0),
terminals: terminal::Sessions::default(),
sealed: sealed::Pages::default(), sealed: sealed::Pages::default(),
watch: watch::Watch::default(), watch: watch::Watch::default(),
dropped: Mutex::new(Vec::new()), dropped: Mutex::new(Vec::new()),
+437
View File
@@ -0,0 +1,437 @@
//! The terminal panel's socket: a shell on a linked device, through the server's app door.
//!
//! The server has two WebSockets for an app (`/api/v2`, behind the app's token):
//!
//! - `/chats/{chat}/terminal/ws` -- an agent chat's shell, the one the web page's panel opens
//! too, so the two share it;
//! - `/devices/{device}/terminal/ws?directory=` -- before the chat exists, on the device and in
//! the folder a new agent chat has chosen. The chat's first message takes that shell over.
//!
//! Both take `cols` and `rows`. Down: the shell's output as binary frames, and text frames
//! `{"t": "ready" | "closed" | "behind" | "error", ...}`. Up: keystrokes as binary,
//! `{"t": "resize", "cols", "rows"}` as text. A refused token closes with 4401.
//!
//! **The token stays here.** The socket is opened in Rust with `Authorization: Bearer` in the
//! handshake, and the page only ever gets the frames (as `terminal` events) and sends keys,
//! sizes and "close" through commands -- the same rule as every other request the page makes.
//! A page cannot open the socket itself: its content security policy allows it no connection
//! but the bridge.
//!
//! One thread per terminal owns its socket: it reads with a short timeout and, between reads,
//! sends what the page queued on its channel. Blocking sockets, no async runtime of its own.
use std::collections::HashMap;
use std::io::ErrorKind;
use std::net::{TcpStream, ToSocketAddrs};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{self, Receiver, Sender, TryRecvError};
use std::sync::Mutex;
use std::time::{Duration, Instant};
use serde::Serialize;
use serde_json::{json, Value};
use tungstenite::http::Uri;
use tungstenite::stream::MaybeTlsStream;
use tungstenite::{ClientRequestBuilder, Error as WsError, HandshakeError, Message, WebSocket};
use crate::server::{base64, ApiError};
/// The server's close code for a token it refused (`api/v2/terminal.py`).
pub const CLOSE_UNAUTHORIZED: u16 = 4401;
/// The most one keystroke message may be: the server's own limit (a paste, at most).
pub const MAX_INPUT: usize = 256 * 1024;
/// The longest directory the server takes (`api/terminal.py` `MAX_DIR_CHARS`).
const MAX_DIR: usize = 4096;
/// How long a read waits before the thread looks at what the page queued.
const POLL: Duration = Duration::from_millis(40);
/// How long a close waits for the server's answer before the socket is simply dropped.
const CLOSE_WAIT: Duration = Duration::from_secs(2);
type Socket = WebSocket<MaybeTlsStream<TcpStream>>;
/// Where the shell is: an agent chat's, or a device's folder before the chat exists.
#[derive(Clone, Debug, PartialEq)]
pub enum Target {
Chat(String),
Device { device: String, directory: String },
}
/// An id as the server makes them (a chat's, a device token's): letters, digits, `-` and `_`.
fn id_ok(id: &str) -> bool {
!id.is_empty()
&& id.len() <= 128
&& id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}
/// A query value, percent-encoded: every byte but the unreserved ones (RFC 3986 §2.3).
pub fn encode(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for b in text.bytes() {
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {
out.push(b as char);
} else {
out.push_str(&format!("%{b:02X}"));
}
}
out
}
/// The socket's address on the app door (`https://host/api/v2` -> `wss://host/api/v2/...`).
pub fn url(app: &str, target: &Target, cols: u16, rows: u16) -> Result<String, ApiError> {
let door = if let Some(rest) = app.strip_prefix("https://") {
format!("wss://{rest}")
} else if let Some(rest) = app.strip_prefix("http://") {
format!("ws://{rest}")
} else {
return Err(ApiError::new(
400,
"The server's address is not http or https.",
));
};
let cols = cols.clamp(2, 1000);
let rows = rows.clamp(1, 500);
match target {
Target::Chat(chat) => {
if !id_ok(chat) {
return Err(ApiError::new(400, "Not a chat."));
}
Ok(format!(
"{door}/chats/{chat}/terminal/ws?cols={cols}&rows={rows}"
))
}
Target::Device { device, directory } => {
if !id_ok(device) {
return Err(ApiError::new(400, "Not a device."));
}
let directory = directory.trim();
if directory.is_empty() || directory.len() > MAX_DIR || directory.contains('\0') {
return Err(ApiError::new(400, "Not a folder."));
}
Ok(format!(
"{door}/devices/{device}/terminal/ws?directory={}&cols={cols}&rows={rows}",
encode(directory)
))
}
}
}
/// What the page asked the socket's thread to do.
pub enum Up {
Keys(Vec<u8>),
Resize(u16, u16),
Close,
}
/// One frame for the page, as the `terminal` event: `output` (the bytes, base64), `control`
/// (the server's JSON), or `end` (how it ended: `{code, reason, status, error}`).
#[derive(Clone, Serialize)]
pub struct Down {
pub id: u64,
pub kind: &'static str,
pub data: Value,
}
/// How a terminal's socket ended. `code` is the close frame's (4401: the token was refused),
/// `status` an HTTP status the handshake was answered with, `error` why it could not connect or
/// broke off.
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct End {
pub code: Option<u16>,
pub reason: String,
pub status: Option<u16>,
pub error: Option<String>,
}
impl End {
fn failed(error: impl Into<String>) -> Self {
End {
error: Some(error.into()),
..End::default()
}
}
/// The server refused the app's token: the page signs in again.
pub fn unauthorized(&self) -> bool {
self.code == Some(CLOSE_UNAUTHORIZED) || self.status == Some(401)
}
}
/// The open terminals: what each one's thread listens to.
#[derive(Default)]
pub struct Sessions {
next: AtomicU64,
open: Mutex<HashMap<u64, Sender<Up>>>,
}
impl Sessions {
/// Open a terminal's socket on its own thread. `emit` gets every frame, the `end` last;
/// the id is the page's handle on it.
pub fn open(&self, url: String, token: String, emit: impl Fn(Down) + Send + 'static) -> u64 {
let id = self.next.fetch_add(1, Ordering::SeqCst) + 1;
let (tx, rx) = mpsc::channel();
self.open.lock().unwrap().insert(id, tx);
std::thread::spawn(move || {
let send = |kind: &'static str, data: Value| emit(Down { id, kind, data });
let end = run(&url, &token, &rx, &send);
send("end", serde_json::to_value(&end).unwrap_or(Value::Null));
});
id
}
/// Hand the page's keys, size or close to the terminal's thread. False: no such terminal.
pub fn send(&self, id: u64, up: Up) -> bool {
self.open
.lock()
.unwrap()
.get(&id)
.is_some_and(|tx| tx.send(up).is_ok())
}
/// Close one terminal (the page's panel let go of it).
pub fn close(&self, id: u64) {
if let Some(tx) = self.open.lock().unwrap().remove(&id) {
let _ = tx.send(Up::Close);
}
}
/// A terminal's thread has ended: forget it.
pub fn forget(&self, id: u64) {
self.open.lock().unwrap().remove(&id);
}
/// Every terminal closed (signed out, or another server chosen).
pub fn close_all(&self) {
for (_, tx) in self.open.lock().unwrap().drain() {
let _ = tx.send(Up::Close);
}
}
}
/// The TCP socket under the WebSocket, to set its read timeout.
fn tcp(ws: &Socket) -> Option<&TcpStream> {
match ws.get_ref() {
MaybeTlsStream::Plain(s) => Some(s),
MaybeTlsStream::Rustls(s) => Some(s.get_ref()),
_ => None,
}
}
fn connect(url: &str, token: &str) -> Result<Socket, End> {
let uri: Uri = url
.parse()
.map_err(|e| End::failed(format!("Not an address: {e}")))?;
let host = uri
.host()
.ok_or_else(|| End::failed("The address has no host."))?
.trim_start_matches('[')
.trim_end_matches(']')
.to_string();
let port = uri
.port_u16()
.unwrap_or(if uri.scheme_str() == Some("wss") {
443
} else {
80
});
let addrs = (host.as_str(), port)
.to_socket_addrs()
.map_err(|e| End::failed(format!("Could not reach the server: {e}")))?;
let mut last = String::from("no address");
let mut stream = None;
for addr in addrs {
match TcpStream::connect_timeout(&addr, Duration::from_secs(10)) {
Ok(s) => {
stream = Some(s);
break;
}
Err(e) => last = e.to_string(),
}
}
let stream =
stream.ok_or_else(|| End::failed(format!("Could not reach the server: {last}")))?;
// The handshake may take its time; reads only poll once it is done.
let _ = stream.set_read_timeout(Some(Duration::from_secs(30)));
let _ = stream.set_nodelay(true);
let request = ClientRequestBuilder::new(uri)
.with_header("Authorization", format!("Bearer {token}"))
.with_header(
"User-Agent",
format!("llembas-desktop/{}", env!("CARGO_PKG_VERSION")),
);
match tungstenite::client_tls(request, stream) {
Ok((ws, _)) => Ok(ws),
Err(HandshakeError::Failure(WsError::Http(res))) => Err(End {
status: Some(res.status().as_u16()),
..End::default()
}),
Err(HandshakeError::Failure(e)) => Err(End::failed(format!("Could not connect: {e}"))),
Err(HandshakeError::Interrupted(_)) => Err(End::failed("The server did not answer.")),
}
}
/// A read that only timed out: nothing came in this poll.
fn idle(e: &WsError) -> bool {
matches!(e, WsError::Io(io) if matches!(io.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut))
}
fn run(url: &str, token: &str, rx: &Receiver<Up>, send: &dyn Fn(&'static str, Value)) -> End {
let mut ws = match connect(url, token) {
Ok(ws) => ws,
Err(end) => return end,
};
match tcp(&ws) {
Some(s) => {
let _ = s.set_read_timeout(Some(POLL));
}
None => return End::failed("An unexpected kind of connection."),
}
let mut closed_by_server: Option<(u16, String)> = None;
let mut closing: Option<Instant> = None;
loop {
// What the page queued, until the server has said goodbye.
if closing.is_none() && closed_by_server.is_none() {
loop {
let sent = match rx.try_recv() {
Ok(Up::Keys(bytes)) if bytes.len() <= MAX_INPUT => {
ws.send(Message::binary(bytes))
}
Ok(Up::Keys(_)) => Ok(()),
Ok(Up::Resize(cols, rows)) => ws.send(Message::text(
json!({ "t": "resize", "cols": cols, "rows": rows }).to_string(),
)),
Ok(Up::Close) | Err(TryRecvError::Disconnected) => {
closing = Some(Instant::now());
let _ = ws.close(None);
break;
}
Err(TryRecvError::Empty) => break,
};
if let Err(e) = sent {
if !idle(&e) {
return End::failed(format!("The connection broke off: {e}"));
}
}
}
}
match ws.read() {
Ok(Message::Binary(bytes)) => send("output", Value::String(base64(&bytes))),
Ok(Message::Text(text)) => {
if let Ok(value) = serde_json::from_str::<Value>(text.as_str()) {
send("control", value);
}
}
Ok(Message::Close(frame)) => {
let (code, reason) = frame
.map(|f| (u16::from(f.code), f.reason.as_str().to_string()))
.unwrap_or((1005, String::new()));
closed_by_server = Some((code, reason));
}
Ok(_) => {}
Err(e) if idle(&e) => {
if closing.is_some_and(|at| at.elapsed() > CLOSE_WAIT) {
break;
}
}
Err(WsError::ConnectionClosed) | Err(WsError::AlreadyClosed) => break,
Err(e) => {
if closing.is_some() || closed_by_server.is_some() {
break;
}
return End::failed(format!("The connection broke off: {e}"));
}
}
}
match closed_by_server {
Some((code, reason)) => End {
code: Some(code),
reason,
..End::default()
},
None => End::default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_chats_terminal_is_on_the_app_door_with_its_size() {
let t = Target::Chat("chat_1-a".into());
assert_eq!(
url("https://ai.example/api/v2", &t, 80, 24).unwrap(),
"wss://ai.example/api/v2/chats/chat_1-a/terminal/ws?cols=80&rows=24"
);
assert_eq!(
url("http://ai.example:8080/sub/api/v2", &t, 0, 9999).unwrap(),
"ws://ai.example:8080/sub/api/v2/chats/chat_1-a/terminal/ws?cols=2&rows=500"
);
}
#[test]
fn a_terminal_before_the_chat_carries_its_folder_encoded() {
let t = Target::Device {
device: "token1".into(),
directory: " /home/dev/my project&x=1 ".into(),
};
assert_eq!(
url("https://ai.example/api/v2", &t, 100, 30).unwrap(),
"wss://ai.example/api/v2/devices/token1/terminal/ws?directory=%2Fhome%2Fdev%2Fmy%20project%26x%3D1&cols=100&rows=30"
);
assert_eq!(encode("C:\\Users\\Ádám"), "C%3A%5CUsers%5C%C3%81d%C3%A1m");
}
#[test]
fn nothing_but_an_id_and_a_folder_reaches_the_address() {
let door = "https://ai.example/api/v2";
for bad in ["", "../me", "a/b", "a?b", "a b", "x#y"] {
assert!(
url(door, &Target::Chat(bad.into()), 80, 24).is_err(),
"{bad}"
);
let dev = Target::Device {
device: bad.into(),
directory: "/tmp".into(),
};
assert!(url(door, &dev, 80, 24).is_err(), "{bad}");
}
for dir in ["", " ", "a\0b"] {
let dev = Target::Device {
device: "token1".into(),
directory: dir.into(),
};
assert!(url(door, &dev, 80, 24).is_err(), "{dir:?}");
}
assert!(url("ftp://ai.example", &Target::Chat("c".into()), 80, 24).is_err());
}
#[test]
fn a_refused_token_is_told_apart_from_a_dropped_line() {
let refused = End {
code: Some(CLOSE_UNAUTHORIZED),
..End::default()
};
assert!(refused.unauthorized());
assert!(End {
status: Some(401),
..End::default()
}
.unauthorized());
assert!(!End::failed("reset").unauthorized());
assert!(!End {
code: Some(1000),
..End::default()
}
.unauthorized());
}
#[test]
fn keys_for_a_closed_terminal_go_nowhere() {
let s = Sessions::default();
assert!(!s.send(7, Up::Keys(b"ls\n".to_vec())));
s.close(7);
s.close_all();
}
}
+40
View File
@@ -10,12 +10,18 @@
// not in the table is a 404, as an older server would answer. // not in the table is a 404, as an older server would answer.
// streams { "/chats/c1/messages/m1/stream": [[event, data], ...] } -- the frames a followed // streams { "/chats/c1/messages/m1/stream": [[event, data], ...] } -- the frames a followed
// reply sends as soon as it is opened. More can be sent later with `__mock.frame()`. // reply sends as soon as it is opened. More can be sent later with `__mock.frame()`.
// terminal [[kind, data], ...] -- what every terminal's socket sends as soon as it is opened
// (`control` frames as the server's JSON, `output` as text, sent base64 as Rust does).
// More with `__mock.shell(kind, data)`; `{ refuse: {status, detail} }` makes
// `terminal_open` fail.
// //
// Nothing here talks to a network: the page's policy allows it none, and this keeps it so. // Nothing here talks to a network: the page's policy allows it none, and this keeps it so.
;(function () { ;(function () {
const config = window.__mockConfig || {} const config = window.__mockConfig || {}
const listeners = new Map() // event name -> [callback] const listeners = new Map() // event name -> [callback]
const streams = new Map() // stream id -> path const streams = new Map() // stream id -> path
const terminals = new Map() // terminal id -> { target, open }
let nextTerminal = 0
let nextStream = 0 let nextStream = 0
const calls = [] const calls = []
@@ -36,6 +42,17 @@
return Promise.reject({ status: 404, detail: `Not in the test's table: ${method} ${path}` }) return Promise.reject({ status: 404, detail: `Not in the test's table: ${method} ${path}` })
} }
// The shell's bytes, base64 as Rust sends them (`terminal.rs`): UTF-8 first.
const b64 = (text) => btoa(String.fromCharCode(...new TextEncoder().encode(text)))
function shell(kind, data, id) {
const ids = id ? [id] : [...terminals.keys()].filter((k) => terminals.get(k).open)
for (const tid of ids) {
if (kind === "end") terminals.get(tid).open = false
dispatch("terminal", { id: tid, kind, data: kind === "output" ? b64(data) : data })
}
}
function frame(path, event, data) { function frame(path, event, data) {
for (const [id, p] of streams) if (p === path) dispatch("stream", { id, event, data }) for (const [id, p] of streams) if (p === path) dispatch("stream", { id, event, data })
} }
@@ -61,6 +78,24 @@
case "stream_close": case "stream_close":
streams.delete(args.id) streams.delete(args.id)
return null return null
case "terminal_open": {
const t = config.terminal || []
if (t.refuse) return Promise.reject(t.refuse)
const id = ++nextTerminal
terminals.set(id, { target: { chat: args.chat, device: args.device, directory: args.directory }, open: true })
setTimeout(() => {
for (const [kind, data] of t) shell(kind, data, id)
}, 0)
return id
}
case "terminal_input":
case "terminal_resize":
return !!terminals.get(args.id)?.open
case "terminal_close": {
const t = terminals.get(args.id)
if (t?.open) setTimeout(() => shell("end", { code: 1000, reason: "", status: null, error: null }, args.id), 0)
return null
}
case "model_image": case "model_image":
return "" return ""
case "terminal_word": case "terminal_word":
@@ -96,5 +131,10 @@
/** The calls of one command, or of `api` with one method and path prefix. */ /** The calls of one command, or of `api` with one method and path prefix. */
api: (method, prefix) => calls.filter((c) => c.cmd === "api" && c.args.method === method && c.args.path.startsWith(prefix)), api: (method, prefix) => calls.filter((c) => c.cmd === "api" && c.args.method === method && c.args.path.startsWith(prefix)),
streams: () => [...streams.values()], streams: () => [...streams.values()],
shell,
/** The terminals opened so far: [{ id, target, open }]. */
terminals: () => [...terminals].map(([id, t]) => ({ id, ...t })),
/** What was typed into a terminal, joined. */
typed: () => calls.filter((c) => c.cmd === "terminal_input").map((c) => c.args.data).join(""),
} }
})() })()
+183 -1
View File
@@ -175,7 +175,7 @@ function baseRoutes(over = {}) {
return { return {
"GET /me": { id: "u1", name: "Ada Example", email: "ada@example.org", theme: "moria", language: "en", permissions: {} }, "GET /me": { id: "u1", name: "Ada Example", email: "ada@example.org", theme: "moria", language: "en", permissions: {} },
"GET /models": models(), "GET /models": models(),
"GET /devices": { devices: [{ id: "token1", name: "workbench", host: "workbench", linked: true }] }, "GET /devices": { devices: [{ id: "token1", name: "workbench", host: "workbench", platform: "linux", terminal: true }] },
"GET /chats": { chats: [chatRow(), agentChat()], more: false }, "GET /chats": { chats: [chatRow(), agentChat()], more: false },
"GET /schedules": { schedules: [] }, "GET /schedules": { schedules: [] },
...over, ...over,
@@ -433,6 +433,188 @@ test("A5: an open chat follows its own `subagents`: offered, offered with the no
} }
}) })
// --- B3: the Terminal panel -------------------------------------------------------------------
//
// The server's two sockets for an app (`api/v2/terminal.py`): an agent chat's
// (`/chats/{chat}/terminal/ws`) and one before the chat exists, on the device and folder chosen
// (`/devices/{device}/terminal/ws?directory=`), which the chat's first message takes over.
// `GET /devices` says per device whether it opens a terminal. The socket is Rust's: the page asks
// `terminal_open` and gets the frames as `terminal` events (mock-tauri.js plays the server).
const READY = ["control", { t: "ready", label: "workbench", dir: "/home/dev/project", cols: 80, rows: 24, shared: false, osc: false }]
function b3(over = {}, terminal = [READY, ["output", "dev@workbench:~/project$ "]]) {
return {
status: status(),
routes: baseRoutes({
"GET /chats/chat2": { chat: agentChat({ running: false }), messages: [userMessage("m1", "What is in build?")], more: false, live: null },
"GET /models?chat=chat2": models(),
"POST /chats": { chat: agentChat({ running: false }), message: userMessage("m9", "Build it"), reply: null },
...over,
}),
streams: {},
terminal,
}
}
async function screenText(p) {
return p.evaluate(() => document.querySelector("#terminal .xterm-rows")?.textContent || "")
}
test("B3: a new agent chat offers the Terminal once its folder is chosen, and opens a shell there before the first message", async () => {
const p = await page(b3())
await p.click("#new-chat-btn")
await p.click(".kind__tab >> text=Agent")
await p.waitForSelector("#composer-chips .cchip--terminal")
assert.equal(await p.evaluate(() => window.__mock.terminals().length), 0, "nothing is opened until asked")
await p.click("#composer-chips .cchip--terminal")
await p.waitForSelector("#terminal:not([hidden]) .xterm")
await p.waitForFunction(() => window.__mock.terminals().length === 1)
const [shell] = await p.evaluate(() => window.__mock.terminals())
assert.deepEqual(shell.target, { chat: null, device: "token1", directory: "/home/dev/project" })
await p.waitForFunction(() => document.querySelector("#terminal .xterm-rows")?.textContent.includes("dev@workbench"))
assert.equal(await p.textContent("#terminal-message"), "Connected.")
assert.equal(await p.textContent("#terminal-title"), "workbench")
assert.equal(await p.getAttribute("#composer-chips .cchip--terminal", "aria-pressed"), "true")
// Keys go up through the bridge, never a socket of the page's own.
await p.click("#terminal .xterm")
await p.keyboard.type("ls")
await p.keyboard.press("Enter")
await p.waitForFunction(() => window.__mock.typed().includes("ls\r"))
assert.equal(await p.evaluate(() => window.__mock.calls.filter((c) => c.cmd === "terminal_open").length), 1)
if (process.env.SHOTS) await p.screenshot({ path: join(process.env.SHOTS, "b3-new-1280.png") })
// The first message makes the chat, which takes the shell over: the same socket goes on.
await p.click("#composer-text")
await p.fill("#composer-text", "Build it")
await p.click("#send")
await p.waitForFunction(() => window.__mock.api("POST", "/chats").length === 1)
const [made] = await p.evaluate(() => window.__mock.api("POST", "/chats"))
assert.equal(made.args.body.device, "token1")
assert.equal(made.args.body.directory, "/home/dev/project")
await p.waitForSelector('#chat-tools [data-terminal-toggle][aria-pressed="true"]')
assert.ok(await p.isVisible("#terminal"), "the panel stays open")
const shells = await p.evaluate(() => window.__mock.terminals())
assert.equal(shells.length, 1, "no second shell: the chat took the first one over")
assert.ok(shells[0].open, "and its socket is still open")
assert.deepEqual(p.errors, [])
})
test("B3: an open agent chat opens its own shell from the header; Send to chat fills the composer and sends nothing", async () => {
const p = await page(b3())
await openChatRow(p, "chat2")
await p.click('#chat-tools [data-terminal-toggle]')
await p.waitForFunction(() => window.__mock.terminals().length === 1)
assert.deepEqual((await p.evaluate(() => window.__mock.terminals()))[0].target, { chat: "chat2", device: null, directory: null })
await p.evaluate(() => window.__mock.shell("output", "\r\ntotal 4\r\n-rw-r--r-- 1 dev dev 12 build.log\r\n"))
await p.waitForFunction(() => document.querySelector("#terminal .xterm-rows")?.textContent.includes("build.log"))
await p.fill("#composer-text", "Look at this:")
await p.click("#terminal-send")
const text = await p.inputValue("#composer-text")
assert.ok(text.startsWith("Look at this:\n\n```\n"), `after what was typed, as a block: ${JSON.stringify(text.slice(0, 40))}`)
assert.match(text, /build\.log/)
assert.equal(await p.evaluate(() => window.__mock.api("POST", "/chats/chat2/messages").length), 0, "nothing is sent")
// Closing the panel keeps the shell; opening it again does not open another.
await p.click("#terminal-close")
assert.ok(await p.isHidden("#terminal"))
await p.click('#chat-tools [data-terminal-toggle]')
await p.waitForSelector("#terminal:not([hidden])")
assert.equal(await p.evaluate(() => window.__mock.terminals().length), 1)
// The server ends it: said, and no new shell in its place.
await p.evaluate(() => window.__mock.shell("control", { t: "closed", message: "The shell exited." }))
await p.waitForFunction(() => document.querySelector("#terminal-message").textContent === "The shell exited.")
assert.equal(await p.evaluate(() => window.__mock.terminals().length), 1)
assert.deepEqual(p.errors, [])
})
test("B3: the terminal and the activity panel take turns in the panel column", async () => {
const p = await page(b3({ "GET /chats/chat2/activity": { activity: {} } }))
await openChatRow(p, "chat2")
await p.click('#chat-tools [data-terminal-toggle]')
await p.waitForSelector("#terminal:not([hidden])")
assert.equal(await p.getAttribute("#shell", "data-terminal"), "true")
await p.click("#activity-open")
await p.waitForSelector("#activity:not([hidden])")
assert.ok(await p.isHidden("#terminal"), "the terminal gave way")
assert.equal(await p.getAttribute("#shell", "data-terminal"), "false")
await p.click('#chat-tools [data-terminal-toggle]')
await p.waitForSelector("#terminal:not([hidden])")
assert.ok(await p.isHidden("#activity"), "and the activity panel to it")
assert.equal(await p.evaluate(() => window.__mock.terminals().length), 1, "the shell lived on behind it")
})
test("B3: no Terminal where the device opens none, for a chat on the server, or before a folder", async () => {
const off = { "GET /devices": { devices: [{ id: "token1", name: "workbench", host: "workbench", platform: "linux", terminal: false }] } }
let p = await page(b3(off))
await p.click("#new-chat-btn")
await p.click(".kind__tab >> text=Agent")
await p.waitForSelector("#composer-chips .cchip")
assert.equal(await p.locator("#composer-chips .cchip--terminal").count(), 0, "the device opens no terminal")
await openChatRow(p, "chat2")
assert.equal(await p.locator("#chat-tools [data-terminal-toggle]").count(), 0)
await p.context().close()
p = await page(b3({}))
await p.click("#new-chat-btn")
await p.click(".kind__tab >> text=Chat")
await p.waitForSelector("#composer-chips .cchip")
assert.equal(await p.locator("#composer-chips .cchip--terminal").count(), 0, "a chat on the server has no shell")
await p.context().close()
p = await page({ ...b3({}), status: status({ remote: { enabled: true, roots: [] } }) })
await p.click("#new-chat-btn")
await p.click(".kind__tab >> text=Agent")
await p.waitForSelector("#composer-chips .cchip")
assert.equal(await p.locator("#composer-chips .cchip--terminal").count(), 0, "no folder yet")
assert.deepEqual(p.errors, [])
})
test("B3: a refused sign-in and an older server are said in words", async () => {
let p = await page(b3({}, [["end", { code: 4401, reason: "not allowed", status: null, error: null }]]))
await openChatRow(p, "chat2")
await p.click('#chat-tools [data-terminal-toggle]')
await p.waitForFunction(() => /Sign in again/.test(document.querySelector("#terminal-message").textContent))
assert.ok(await p.evaluate(() => document.querySelector("#terminal-message").classList.contains("is-error")))
await p.context().close()
p = await page(b3({}, [["end", { code: null, reason: "", status: 404, error: null }]]))
await openChatRow(p, "chat2")
await p.click('#chat-tools [data-terminal-toggle]')
await p.waitForFunction(() => /needs a newer LLeMbas server/.test(document.querySelector("#terminal-message").textContent))
assert.deepEqual(p.errors, [])
})
test("B3: at 390 px the terminal is a sheet over the chat, inside the window", async () => {
const p = await page(b3(), { width: 390, height: 844 })
await p.evaluate(() => document.querySelector('.chatrow[data-id="chat2"]').click())
await p.waitForSelector('#chat-tools [data-terminal-toggle]')
await p.click('#chat-tools [data-terminal-toggle]')
await p.waitForFunction(() => document.querySelector("#terminal .xterm-rows")?.textContent.includes("dev@workbench"))
const box = await p.locator("#terminal").boundingBox()
assert.ok(box.x >= 0 && box.x + box.width <= 390 && box.y + box.height <= 844, `inside: ${JSON.stringify(box)}`)
const send = await p.locator("#terminal-send").boundingBox()
assert.ok(send.x + send.width <= 390 && send.y + send.height <= 844, `Send is reachable: ${JSON.stringify(send)}`)
const close = await p.locator("#terminal-close").boundingBox()
assert.ok(close.x + close.width <= 390, "Close is reachable")
assert.ok(await p.evaluate(() => document.documentElement.scrollWidth <= 390), "nothing scrolls sideways")
if (process.env.SHOTS) await p.screenshot({ path: join(process.env.SHOTS, "b3-chat-390.png") })
// Send to chat: the sheet steps aside, and the message box has the block.
await p.click("#terminal-send")
assert.ok(await p.isHidden("#terminal"))
assert.match(await p.inputValue("#composer-text"), /dev@workbench/)
assert.deepEqual(p.errors, [])
})
test("B3: at 1280 px the terminal sits beside the chat, which keeps its column", async () => {
const p = await page(b3())
await openChatRow(p, "chat2")
await p.click('#chat-tools [data-terminal-toggle]')
await p.waitForFunction(() => document.querySelector("#terminal .xterm-rows")?.textContent.includes("dev@workbench"))
const term = await p.locator("#terminal").boundingBox()
const main = await p.locator("#main").boundingBox()
assert.ok(term.x >= main.x + main.width - 1, "beside, not over")
assert.ok(main.width >= 400, `the chat keeps room: ${main.width}`)
assert.ok(term.width >= 380, `the terminal has its width: ${term.width}`)
if (process.env.SHOTS) await p.screenshot({ path: join(process.env.SHOTS, "b3-chat-1280.png") })
assert.deepEqual(p.errors, [])
})
// --- Run ---------------------------------------------------------------------------------------- // --- Run ----------------------------------------------------------------------------------------
const only = process.argv[2] || "" const only = process.argv[2] || ""
+58 -2
View File
@@ -327,6 +327,8 @@ label, .label { font: var(--text-sm) / var(--leading-tight) var(--font-body); co
.shell[data-side="false"] { --side-col: 0px; } .shell[data-side="false"] { --side-col: 0px; }
.shell[data-viewer="true"] { --panel-col: minmax(var(--inspector-width-min), 42%); } .shell[data-viewer="true"] { --panel-col: minmax(var(--inspector-width-min), 42%); }
.shell[data-activity="true"] { --panel-col: var(--inspector-width); } .shell[data-activity="true"] { --panel-col: var(--inspector-width); }
/* The terminal: the server's width for it, giving way down to its least before the chat does. */
.shell[data-terminal="true"] { --panel-col: minmax(var(--terminal-width-min), var(--terminal-width)); }
/* --- The sidebar --------------------------------------------------------------------------- */ /* --- The sidebar --------------------------------------------------------------------------- */
@@ -1291,6 +1293,7 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
} }
.cchip:hover { color: var(--ink); background: var(--surface-hover); border-color: var(--border); } .cchip:hover { color: var(--ink); background: var(--surface-hover); border-color: var(--border); }
.cchip:disabled { cursor: default; } .cchip:disabled { cursor: default; }
.cchip[aria-pressed="true"], .tool[aria-pressed="true"] { color: var(--ink); background: var(--surface-active); }
.cchip:disabled:hover { background: transparent; border-color: transparent; } .cchip:disabled:hover { background: transparent; border-color: transparent; }
.cchip__label { overflow: hidden; text-overflow: ellipsis; } .cchip__label { overflow: hidden; text-overflow: ellipsis; }
.cchip__chev { display: grid; color: var(--ink-faint); } .cchip__chev { display: grid; color: var(--ink-faint); }
@@ -1558,12 +1561,65 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
.folders .path { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; direction: rtl; text-align: left; font: var(--text-sm) var(--font-ui); } .folders .path { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; direction: rtl; text-align: left; font: var(--text-sm) var(--font-ui); }
.folders .empty { display: block; padding: var(--sp-3); color: var(--warning); font-size: var(--text-sm); text-align: left; } .folders .empty { display: block; padding: var(--sp-3); color: var(--warning); font-size: var(--text-sm); text-align: left; }
/* --- The terminal panel -------------------------------------------------------------------------
The panel column, as the activity panel has it, and never beside it (opening one closes the
other; the shell lives on). The viewer goes over the chat while it is open, as with the
activity panel. Not `.terminal`: xterm gives its own element that class. The head is the
viewer's: an icon, the device's name over the folder, then the buttons on known tracks (the
command ones only while the shell marks its commands). */
.shell[data-terminal="true"] .viewer { grid-area: main; z-index: var(--z-panel); border-left: 0; }
.termpanel {
grid-area: panel;
min-width: 0;
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
border-left: var(--border-w) solid var(--border);
background: var(--bg-sunken);
}
.termpanel__head {
grid-template-columns: 16px minmax(0, 1fr);
grid-auto-flow: column;
grid-auto-columns: var(--control-h-sm);
}
.termpanel__icon { display: grid; place-items: center; color: var(--ink-muted); }
.termpanel__where { font-family: var(--font-mono); }
/* xterm measures this box: it has a size of its own, and the terminal scrolls inside it. */
.termpanel__screen { min-height: 0; min-width: 0; overflow: hidden; padding: var(--sp-2); background: var(--code-bg); }
.termpanel__screen .xterm { height: 100%; }
/* What happened, the last command, and Send: the words take what is left and wrap; the last
command is one clipped line; the button keeps its size. */
.termpanel__foot {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-auto-flow: column;
grid-auto-columns: auto;
gap: var(--sp-2);
align-items: center;
padding: var(--sp-2) var(--sp-3);
border-top: var(--border-w) solid var(--border);
font-size: var(--text-xs);
color: var(--ink-faint);
}
.termpanel__message { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.termpanel__message.is-error { color: var(--danger); }
.termpanel__last {
max-width: var(--w-chip);
font-family: var(--font-mono);
color: var(--ink-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.termpanel__last:empty { display: none; }
/* --- Narrower windows ---------------------------------------------------------------------- */ /* --- Narrower windows ---------------------------------------------------------------------- */
/* 64rem (--bp-wide): a panel goes over the chat instead of beside it. */ /* 64rem (--bp-wide): a panel goes over the chat instead of beside it. */
@media (max-width: 64rem) { @media (max-width: 64rem) {
.shell[data-viewer="true"], .shell[data-activity="true"] { --panel-col: 0px; } .shell[data-viewer="true"], .shell[data-activity="true"], .shell[data-terminal="true"] { --panel-col: 0px; }
.viewer, .activity, .shell[data-activity="true"] .viewer { grid-area: main; z-index: var(--z-panel); border-left: 0; } .viewer, .activity, .termpanel, .shell[data-activity="true"] .viewer, .shell[data-terminal="true"] .viewer { grid-area: main; z-index: var(--z-panel); border-left: 0; }
.task__open { grid-template-columns: var(--sp-8) minmax(0, 1fr) var(--w-field-sm); } .task__open { grid-template-columns: var(--sp-8) minmax(0, 1fr) var(--w-field-sm); }
.task__next { display: none; } .task__next { display: none; }
} }
+176
View File
@@ -68,6 +68,7 @@ const LEMBAS_I18N = {
"Choose a model to choose its effort.": "Vyberte model, abyste mohli zvolit jeho náročnost.", "Choose a model to choose its effort.": "Vyberte model, abyste mohli zvolit jeho náročnost.",
"Close": "Zavřít", "Close": "Zavřít",
"Close (LLeMbas stays in the tray)": "Zavřít (LLeMbas zůstane v oznamovací oblasti)", "Close (LLeMbas stays in the tray)": "Zavřít (LLeMbas zůstane v oznamovací oblasti)",
"Close terminal": "Zavřít terminál",
"Close the activity panel": "Zavřít panel aktivity", "Close the activity panel": "Zavřít panel aktivity",
"Command": "Příkaz", "Command": "Příkaz",
"Commands": "Příkazy", "Commands": "Příkazy",
@@ -77,12 +78,18 @@ const LEMBAS_I18N = {
"Compacts before the next reply.": "Shrne se před další odpovědí.", "Compacts before the next reply.": "Shrne se před další odpovědí.",
"Compacts by itself once the window is known.": "Shrne se samo, jakmile bude známé okno.", "Compacts by itself once the window is known.": "Shrne se samo, jakmile bude známé okno.",
"Connect": "Připojit", "Connect": "Připojit",
"Connected.": "Připojeno.",
"Connected. This shell is also open in another tab, and they share a size.": "Připojeno. Tento shell je otevřený i na jiné kartě a sdílejí velikost.",
"Connecting…": "Připojuje se…",
"Context": "Kontext", "Context": "Kontext",
"Context and background tasks": "Kontext a úlohy na pozadí", "Context and background tasks": "Kontext a úlohy na pozadí",
"Continue in a terminal": "Pokračovat v terminálu", "Continue in a terminal": "Pokračovat v terminálu",
"Continue in a terminal…": "Pokračovat v terminálu…", "Continue in a terminal…": "Pokračovat v terminálu…",
"Copied": "Zkopírováno", "Copied": "Zkopírováno",
"Copied the last of the screen, as it appeared. This shell does not mark where commands begin.": "Zkopírován konec obrazovky, tak jak vypadal. Tento shell neoznačuje, kde příkazy začínají.",
"Copied.": "Zkopírováno.",
"Copy": "Kopírovat", "Copy": "Kopírovat",
"Copy the last command and its output": "Zkopírovat poslední příkaz a jeho výstup",
"Counted by the model at the last reply; the categories are estimated.": "Spočítal model při poslední odpovědi; kategorie jsou odhadnuté.", "Counted by the model at the last reply; the categories are estimated.": "Spočítal model při poslední odpovědi; kategorie jsou odhadnuté.",
"Cut short here; the whole of it is in the web interface.": "Zde zkráceno; celé je to ve webovém rozhraní.", "Cut short here; the whole of it is in the web interface.": "Zde zkráceno; celé je to ve webovém rozhraní.",
"Days": "Dny", "Days": "Dny",
@@ -91,6 +98,7 @@ const LEMBAS_I18N = {
"Delete this chat?": "Smazat tuto konverzaci?", "Delete this chat?": "Smazat tuto konverzaci?",
"Delete this scheduled task?": "Odstranit tuto naplánovanou úlohu?", "Delete this scheduled task?": "Odstranit tuto naplánovanou úlohu?",
"Deny": "Zamítnout", "Deny": "Zamítnout",
"Disconnected. Close and reopen to reconnect.": "Odpojeno. Pro opětovné připojení zavřete a znovu otevřete.",
"Done": "Hotovo", "Done": "Hotovo",
"Drop the files to attach them": "Pusťte soubory a přiloží se", "Drop the files to attach them": "Pusťte soubory a přiloží se",
"Due %(when)s": "Naplánováno na %(when)s", "Due %(when)s": "Naplánováno na %(when)s",
@@ -165,12 +173,14 @@ const LEMBAS_I18N = {
"Not measured.": "Neměřeno.", "Not measured.": "Neměřeno.",
"Not run yet": "Ještě neběžela", "Not run yet": "Ještě neběžela",
"Not yet": "Ještě ne", "Not yet": "Ještě ne",
"Nothing has been run in this shell yet.": "V tomto shellu se zatím nic nespustilo.",
"Nothing has been said in the room yet.": "V místnosti zatím nikdo nic neřekl.", "Nothing has been said in the room yet.": "V místnosti zatím nikdo nic neřekl.",
"Nothing is scheduled.": "Nic není naplánováno.", "Nothing is scheduled.": "Nic není naplánováno.",
"Nothing matches.": "Nic neodpovídá.", "Nothing matches.": "Nic neodpovídá.",
"Nothing running.": "Nic neběží.", "Nothing running.": "Nic neběží.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Na pozadí nic neběží. Podagenti, členové týmu a příkazy na pozadí se tu zobrazí, dokud pracují.", "Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Na pozadí nic neběží. Podagenti, členové týmu a příkazy na pozadí se tu zobrazí, dokud pracují.",
"Nothing to report": "Nic k hlášení", "Nothing to report": "Nic k hlášení",
"Nothing to send: select some output first.": "Není co odeslat: nejdřív označte část výstupu.",
"Nothing yet.": "Zatím nic.", "Nothing yet.": "Zatím nic.",
"Notifications": "Upozornění", "Notifications": "Upozornění",
"One chat that grows": "Jedna konverzace, která roste", "One chat that grows": "Jedna konverzace, která roste",
@@ -192,9 +202,11 @@ const LEMBAS_I18N = {
"Plan — changes nothing": "Plan — nic nemění", "Plan — changes nothing": "Plan — nic nemění",
"Preview": "Náhled", "Preview": "Náhled",
"Project folders": "Složky projektů", "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",
"Read again": "Načíst znovu", "Read again": "Načíst znovu",
"Reasoning effort": "Náročnost uvažování", "Reasoning effort": "Náročnost uvažování",
"Recommended": "Doporučeno", "Recommended": "Doporučeno",
"Reconnecting…": "Znovu se připojuje…",
"Regenerate reply": "Vygenerovat odpověď znovu", "Regenerate reply": "Vygenerovat odpověď znovu",
"Remove": "Odebrat", "Remove": "Odebrat",
"Remove %(dir)s": "Odebrat %(dir)s", "Remove %(dir)s": "Odebrat %(dir)s",
@@ -220,6 +232,7 @@ const LEMBAS_I18N = {
"Search": "Hledat", "Search": "Hledat",
"Search chats": "Hledat v konverzacích", "Search chats": "Hledat v konverzacích",
"Send": "Odeslat", "Send": "Odeslat",
"Send to chat": "Odeslat do konverzace",
"Sending…": "Odesílá se…", "Sending…": "Odesílá se…",
"Server": "Server", "Server": "Server",
"Set the time yourself": "Nastavit čas sami", "Set the time yourself": "Nastavit čas sami",
@@ -228,6 +241,7 @@ const LEMBAS_I18N = {
"Shown by the agent": "Ukázal agent", "Shown by the agent": "Ukázal agent",
"Sidebar": "Postranní panel", "Sidebar": "Postranní panel",
"Sign in": "Přihlásit se", "Sign in": "Přihlásit se",
"Sign in again: the server no longer accepts this app's sign-in.": "Přihlaste se znovu: server už nepřijímá přihlášení této aplikace.",
"Sign out": "Odhlásit se", "Sign out": "Odhlásit se",
"Signed in as %(who)s.": "Přihlášen jako %(who)s.", "Signed in as %(who)s.": "Přihlášen jako %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Odhlášením se přihlášení aplikace na serveru zruší; propojení agenta zůstane. Chcete-li použít jiný server, odhlaste se.", "Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Odhlášením se přihlášení aplikace na serveru zruší; propojení agenta zůstane. Chcete-li použít jiný server, odhlaste se.",
@@ -250,6 +264,7 @@ const LEMBAS_I18N = {
"Team: %(size)s of at most %(max)s subagents": "Tým: %(size)s z nejvýše %(max)s podagentů", "Team: %(size)s of at most %(max)s subagents": "Tým: %(size)s z nejvýše %(max)s podagentů",
"Tell me": "Upozornit mě", "Tell me": "Upozornit mě",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Upozornit mě, když je odpověď nebo práce agenta hotová, zatímco jsem jinde", "Tell me when a reply or an agent's work is finished while I am elsewhere": "Upozornit mě, když je odpověď nebo práce agenta hotová, zatímco jsem jinde",
"Terminal": "Terminál",
"The agent asks to run %(tool)s:": "Agent žádá o spuštění %(tool)s:", "The agent asks to run %(tool)s:": "Agent žádá o spuštění %(tool)s:",
"The agent asks:": "Agent se ptá:", "The agent asks:": "Agent se ptá:",
"The agent has finished.": "Agent dokončil práci.", "The agent has finished.": "Agent dokončil práci.",
@@ -269,11 +284,17 @@ const LEMBAS_I18N = {
"The last %(n)s runs failed: %(why)s": "Posledních %(n)s spuštění selhalo: %(why)s", "The last %(n)s runs failed: %(why)s": "Posledních %(n)s spuštění selhalo: %(why)s",
"The model's own": "Podle modelu", "The model's own": "Podle modelu",
"The most an agent chat may do here": "Nejvíc, co tu agentní konverzace smí", "The most an agent chat may do here": "Nejvíc, co tu agentní konverzace smí",
"The next command": "Další příkaz",
"The next command (Ctrl+Shift+↓)": "Další příkaz (Ctrl+Shift+↓)",
"The next runs": "Další spuštění", "The next runs": "Další spuštění",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Čísla počítají i vlastního agenta konverzace: 1 je agent sám, bez podagentů; Max 4 je agent a až 3 podagenti. Podagent běží na modelu konverzace, na jiném jen tehdy, když ho v konverzaci pojmenujete.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Čísla počítají i vlastního agenta konverzace: 1 je agent sám, bez podagentů; Max 4 je agent a až 3 podagenti. Podagent běží na modelu konverzace, na jiném jen tehdy, když ho v konverzaci pojmenujete.",
"The previous command": "Předchozí příkaz",
"The previous command (Ctrl+Shift+↑)": "Předchozí příkaz (Ctrl+Shift+↑)",
"The reply is ready.": "Odpověď je hotová.", "The reply is ready.": "Odpověď je hotová.",
"The result is no longer there.": "Výsledek už tu není.", "The result is no longer there.": "Výsledek už tu není.",
"The server refused this computer's link: %(why)s": "Server odmítl propojení tohoto počítače: %(why)s", "The server refused this computer's link: %(why)s": "Server odmítl propojení tohoto počítače: %(why)s",
"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í.", "These words give no run in the future.": "Z těchto slov nevychází žádné budoucí spuštění.",
"Think before answering": "Přemýšlet před odpovědí", "Think before answering": "Přemýšlet před odpovědí",
"Thinking": "Přemýšlí", "Thinking": "Přemýšlí",
@@ -286,6 +307,7 @@ const LEMBAS_I18N = {
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Tento model vyřizuje najednou jeden požadavek: podagenti potřebují jiný model na jiném serveru, pojmenovaný v konverzaci.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Tento model vyřizuje najednou jeden požadavek: podagenti potřebují jiný model na jiném serveru, pojmenovaný v konverzaci.",
"This server cannot compact a chat from the app yet; the web interface can.": "Tento server zatím neumí shrnout konverzaci z aplikace; webové rozhraní to umí.", "This server cannot compact a chat from the app yet; the web interface can.": "Tento server zatím neumí shrnout konverzaci z aplikace; webové rozhraní to umí.",
"This server has no scheduled tasks yet.": "Tento server zatím nemá naplánované úlohy.", "This server has no scheduled tasks yet.": "Tento server zatím nemá naplánované úlohy.",
"This terminal closed.": "Tento terminál se zavřel.",
"Thought": "Přemýšlel", "Thought": "Přemýšlel",
"Time": "Čas", "Time": "Čas",
"Times in %(zone)s": "Časové pásmo: %(zone)s", "Times in %(zone)s": "Časové pásmo: %(zone)s",
@@ -399,6 +421,7 @@ const LEMBAS_I18N = {
"Choose a model to choose its effort.": "Wählen Sie ein Modell, um seinen Aufwand zu wählen.", "Choose a model to choose its effort.": "Wählen Sie ein Modell, um seinen Aufwand zu wählen.",
"Close": "Schließen", "Close": "Schließen",
"Close (LLeMbas stays in the tray)": "Schließen (LLeMbas bleibt im Benachrichtigungsbereich)", "Close (LLeMbas stays in the tray)": "Schließen (LLeMbas bleibt im Benachrichtigungsbereich)",
"Close terminal": "Terminal schließen",
"Close the activity panel": "Aktivitätsbereich schließen", "Close the activity panel": "Aktivitätsbereich schließen",
"Command": "Befehl", "Command": "Befehl",
"Commands": "Befehle", "Commands": "Befehle",
@@ -408,12 +431,18 @@ const LEMBAS_I18N = {
"Compacts before the next reply.": "Wird vor der nächsten Antwort zusammengefasst.", "Compacts before the next reply.": "Wird vor der nächsten Antwort zusammengefasst.",
"Compacts by itself once the window is known.": "Wird von selbst zusammengefasst, sobald das Fenster bekannt ist.", "Compacts by itself once the window is known.": "Wird von selbst zusammengefasst, sobald das Fenster bekannt ist.",
"Connect": "Verbinden", "Connect": "Verbinden",
"Connected.": "Verbunden.",
"Connected. This shell is also open in another tab, and they share a size.": "Verbunden. Diese Shell ist auch in einem anderen Tab offen, und beide teilen sich eine Größe.",
"Connecting…": "Verbinde…",
"Context": "Kontext", "Context": "Kontext",
"Context and background tasks": "Kontext und Hintergrundaufgaben", "Context and background tasks": "Kontext und Hintergrundaufgaben",
"Continue in a terminal": "Im Terminal fortsetzen", "Continue in a terminal": "Im Terminal fortsetzen",
"Continue in a terminal…": "Im Terminal fortsetzen…", "Continue in a terminal…": "Im Terminal fortsetzen…",
"Copied": "Kopiert", "Copied": "Kopiert",
"Copied the last of the screen, as it appeared. This shell does not mark where commands begin.": "Das Ende des Bildschirms so kopiert, wie es erschien. Diese Shell markiert nicht, wo Befehle beginnen.",
"Copied.": "Kopiert.",
"Copy": "Kopieren", "Copy": "Kopieren",
"Copy the last command and its output": "Den letzten Befehl und seine Ausgabe kopieren",
"Counted by the model at the last reply; the categories are estimated.": "Vom Modell bei der letzten Antwort gezählt; die Kategorien sind geschätzt.", "Counted by the model at the last reply; the categories are estimated.": "Vom Modell bei der letzten Antwort gezählt; die Kategorien sind geschätzt.",
"Cut short here; the whole of it is in the web interface.": "Hier gekürzt; vollständig steht es in der Weboberfläche.", "Cut short here; the whole of it is in the web interface.": "Hier gekürzt; vollständig steht es in der Weboberfläche.",
"Days": "Tage", "Days": "Tage",
@@ -422,6 +451,7 @@ const LEMBAS_I18N = {
"Delete this chat?": "Diesen Chat löschen?", "Delete this chat?": "Diesen Chat löschen?",
"Delete this scheduled task?": "Diese geplante Aufgabe löschen?", "Delete this scheduled task?": "Diese geplante Aufgabe löschen?",
"Deny": "Ablehnen", "Deny": "Ablehnen",
"Disconnected. Close and reopen to reconnect.": "Getrennt. Schließen und erneut öffnen, um neu zu verbinden.",
"Done": "Fertig", "Done": "Fertig",
"Drop the files to attach them": "Dateien ablegen, um sie anzuhängen", "Drop the files to attach them": "Dateien ablegen, um sie anzuhängen",
"Due %(when)s": "Fällig %(when)s", "Due %(when)s": "Fällig %(when)s",
@@ -496,12 +526,14 @@ const LEMBAS_I18N = {
"Not measured.": "Nicht gemessen.", "Not measured.": "Nicht gemessen.",
"Not run yet": "Noch nicht gelaufen", "Not run yet": "Noch nicht gelaufen",
"Not yet": "Noch nicht", "Not yet": "Noch nicht",
"Nothing has been run in this shell yet.": "In dieser Shell wurde noch nichts ausgeführt.",
"Nothing has been said in the room yet.": "Im Raum wurde noch nichts gesagt.", "Nothing has been said in the room yet.": "Im Raum wurde noch nichts gesagt.",
"Nothing is scheduled.": "Nichts ist geplant.", "Nothing is scheduled.": "Nichts ist geplant.",
"Nothing matches.": "Nichts gefunden.", "Nothing matches.": "Nichts gefunden.",
"Nothing running.": "Nichts läuft.", "Nothing running.": "Nichts läuft.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Im Hintergrund läuft nichts. Subagenten, die Mitglieder eines Teams und Hintergrundbefehle erscheinen hier, solange sie arbeiten.", "Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Im Hintergrund läuft nichts. Subagenten, die Mitglieder eines Teams und Hintergrundbefehle erscheinen hier, solange sie arbeiten.",
"Nothing to report": "Nichts zu melden", "Nothing to report": "Nichts zu melden",
"Nothing to send: select some output first.": "Nichts zu senden: Markieren Sie zuerst etwas Ausgabe.",
"Nothing yet.": "Noch nichts.", "Nothing yet.": "Noch nichts.",
"Notifications": "Benachrichtigungen", "Notifications": "Benachrichtigungen",
"One chat that grows": "Ein Chat, der wächst", "One chat that grows": "Ein Chat, der wächst",
@@ -523,9 +555,11 @@ const LEMBAS_I18N = {
"Plan — changes nothing": "Plan — ändert nichts", "Plan — changes nothing": "Plan — ändert nichts",
"Preview": "Vorschau", "Preview": "Vorschau",
"Project folders": "Projektordner", "Project folders": "Projektordner",
"Put the last command and its output into the message box": "Den letzten Befehl und seine Ausgabe ins Nachrichtenfeld legen",
"Read again": "Neu lesen", "Read again": "Neu lesen",
"Reasoning effort": "Denkaufwand", "Reasoning effort": "Denkaufwand",
"Recommended": "Empfohlen", "Recommended": "Empfohlen",
"Reconnecting…": "Verbinde neu…",
"Regenerate reply": "Antwort neu generieren", "Regenerate reply": "Antwort neu generieren",
"Remove": "Entfernen", "Remove": "Entfernen",
"Remove %(dir)s": "%(dir)s entfernen", "Remove %(dir)s": "%(dir)s entfernen",
@@ -551,6 +585,7 @@ const LEMBAS_I18N = {
"Search": "Suchen", "Search": "Suchen",
"Search chats": "Chats durchsuchen", "Search chats": "Chats durchsuchen",
"Send": "Senden", "Send": "Senden",
"Send to chat": "An den Chat senden",
"Sending…": "Wird gesendet…", "Sending…": "Wird gesendet…",
"Server": "Server", "Server": "Server",
"Set the time yourself": "Zeit selbst festlegen", "Set the time yourself": "Zeit selbst festlegen",
@@ -559,6 +594,7 @@ const LEMBAS_I18N = {
"Shown by the agent": "Vom Agenten gezeigt", "Shown by the agent": "Vom Agenten gezeigt",
"Sidebar": "Seitenleiste", "Sidebar": "Seitenleiste",
"Sign in": "Anmelden", "Sign in": "Anmelden",
"Sign in again: the server no longer accepts this app's sign-in.": "Melden Sie sich erneut an: Der Server nimmt die Anmeldung dieser App nicht mehr an.",
"Sign out": "Abmelden", "Sign out": "Abmelden",
"Signed in as %(who)s.": "Angemeldet als %(who)s.", "Signed in as %(who)s.": "Angemeldet als %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Beim Abmelden gibt die App ihre Anmeldung am Server auf; die Verbindung des Agenten bleibt. Melden Sie sich ab, um einen anderen Server zu verwenden.", "Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Beim Abmelden gibt die App ihre Anmeldung am Server auf; die Verbindung des Agenten bleibt. Melden Sie sich ab, um einen anderen Server zu verwenden.",
@@ -581,6 +617,7 @@ const LEMBAS_I18N = {
"Team: %(size)s of at most %(max)s subagents": "Team: %(size)s von höchstens %(max)s Subagenten", "Team: %(size)s of at most %(max)s subagents": "Team: %(size)s von höchstens %(max)s Subagenten",
"Tell me": "Benachrichtigen", "Tell me": "Benachrichtigen",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Benachrichtigen, wenn eine Antwort oder die Arbeit des Agenten fertig ist, während ich woanders bin", "Tell me when a reply or an agent's work is finished while I am elsewhere": "Benachrichtigen, wenn eine Antwort oder die Arbeit des Agenten fertig ist, während ich woanders bin",
"Terminal": "Terminal",
"The agent asks to run %(tool)s:": "Der Agent möchte %(tool)s ausführen:", "The agent asks to run %(tool)s:": "Der Agent möchte %(tool)s ausführen:",
"The agent asks:": "Der Agent fragt:", "The agent asks:": "Der Agent fragt:",
"The agent has finished.": "Der Agent ist fertig.", "The agent has finished.": "Der Agent ist fertig.",
@@ -600,11 +637,17 @@ const LEMBAS_I18N = {
"The last %(n)s runs failed: %(why)s": "Die letzten %(n)s Läufe sind fehlgeschlagen: %(why)s", "The last %(n)s runs failed: %(why)s": "Die letzten %(n)s Läufe sind fehlgeschlagen: %(why)s",
"The model's own": "Die des Modells", "The model's own": "Die des Modells",
"The most an agent chat may do here": "Das Meiste, was ein Agent-Chat hier darf", "The most an agent chat may do here": "Das Meiste, was ein Agent-Chat hier darf",
"The next command": "Nächster Befehl",
"The next command (Ctrl+Shift+↓)": "Nächster Befehl (Ctrl+Shift+↓)",
"The next runs": "Die nächsten Läufe", "The next runs": "Die nächsten Läufe",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Die Zahlen zählen den eigenen Agenten des Chats mit: 1 ist der Agent allein, ohne Subagenten; Max 4 ist der Agent und bis zu 3 Subagenten. Ein Subagent läuft auf dem Modell des Chats, auf einem anderen nur, wenn Sie es im Chat nennen.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Die Zahlen zählen den eigenen Agenten des Chats mit: 1 ist der Agent allein, ohne Subagenten; Max 4 ist der Agent und bis zu 3 Subagenten. Ein Subagent läuft auf dem Modell des Chats, auf einem anderen nur, wenn Sie es im Chat nennen.",
"The previous command": "Vorheriger Befehl",
"The previous command (Ctrl+Shift+↑)": "Vorheriger Befehl (Ctrl+Shift+↑)",
"The reply is ready.": "Die Antwort ist fertig.", "The reply is ready.": "Die Antwort ist fertig.",
"The result is no longer there.": "Das Ergebnis ist nicht mehr da.", "The result is no longer there.": "Das Ergebnis ist nicht mehr da.",
"The server refused this computer's link: %(why)s": "Der Server hat die Verknüpfung dieses Computers abgelehnt: %(why)s", "The server refused this computer's link: %(why)s": "Der Server hat die Verknüpfung dieses Computers abgelehnt: %(why)s",
"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.", "These words give no run in the future.": "Diese Worte ergeben keinen künftigen Lauf.",
"Think before answering": "Vor der Antwort nachdenken", "Think before answering": "Vor der Antwort nachdenken",
"Thinking": "Denkt nach", "Thinking": "Denkt nach",
@@ -617,6 +660,7 @@ const LEMBAS_I18N = {
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Dieses Modell bedient eine Anfrage zur Zeit: Subagenten brauchen ein anderes Modell auf einem anderen Server, im Chat genannt.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Dieses Modell bedient eine Anfrage zur Zeit: Subagenten brauchen ein anderes Modell auf einem anderen Server, im Chat genannt.",
"This server cannot compact a chat from the app yet; the web interface can.": "Dieser Server kann einen Chat noch nicht aus der App zusammenfassen; die Weboberfläche kann es.", "This server cannot compact a chat from the app yet; the web interface can.": "Dieser Server kann einen Chat noch nicht aus der App zusammenfassen; die Weboberfläche kann es.",
"This server has no scheduled tasks yet.": "Dieser Server hat noch keine geplanten Aufgaben.", "This server has no scheduled tasks yet.": "Dieser Server hat noch keine geplanten Aufgaben.",
"This terminal closed.": "Dieses Terminal wurde geschlossen.",
"Thought": "Nachgedacht", "Thought": "Nachgedacht",
"Time": "Uhrzeit", "Time": "Uhrzeit",
"Times in %(zone)s": "Zeitzone: %(zone)s", "Times in %(zone)s": "Zeitzone: %(zone)s",
@@ -730,6 +774,7 @@ const LEMBAS_I18N = {
"Choose a model to choose its effort.": "Elija un modelo para elegir su esfuerzo.", "Choose a model to choose its effort.": "Elija un modelo para elegir su esfuerzo.",
"Close": "Cerrar", "Close": "Cerrar",
"Close (LLeMbas stays in the tray)": "Cerrar (LLeMbas sigue en la bandeja del sistema)", "Close (LLeMbas stays in the tray)": "Cerrar (LLeMbas sigue en la bandeja del sistema)",
"Close terminal": "Cerrar terminal",
"Close the activity panel": "Cerrar el panel de actividad", "Close the activity panel": "Cerrar el panel de actividad",
"Command": "Comando", "Command": "Comando",
"Commands": "Comandos", "Commands": "Comandos",
@@ -739,12 +784,18 @@ const LEMBAS_I18N = {
"Compacts before the next reply.": "Se compacta antes de la próxima respuesta.", "Compacts before the next reply.": "Se compacta antes de la próxima respuesta.",
"Compacts by itself once the window is known.": "Se compacta sola en cuanto se conozca la ventana.", "Compacts by itself once the window is known.": "Se compacta sola en cuanto se conozca la ventana.",
"Connect": "Conectar", "Connect": "Conectar",
"Connected.": "Conectado.",
"Connected. This shell is also open in another tab, and they share a size.": "Conectado. Este shell también está abierto en otra pestaña, y comparten tamaño.",
"Connecting…": "Conectando…",
"Context": "Contexto", "Context": "Contexto",
"Context and background tasks": "Contexto y tareas en segundo plano", "Context and background tasks": "Contexto y tareas en segundo plano",
"Continue in a terminal": "Continuar en un terminal", "Continue in a terminal": "Continuar en un terminal",
"Continue in a terminal…": "Continuar en un terminal…", "Continue in a terminal…": "Continuar en un terminal…",
"Copied": "Copiado", "Copied": "Copiado",
"Copied the last of the screen, as it appeared. This shell does not mark where commands begin.": "Se ha copiado el final de la pantalla, tal como aparecía. Este shell no marca dónde empiezan los comandos.",
"Copied.": "Copiado.",
"Copy": "Copiar", "Copy": "Copiar",
"Copy the last command and its output": "Copiar el último comando y su salida",
"Counted by the model at the last reply; the categories are estimated.": "Contado por el modelo en la última respuesta; las categorías son estimaciones.", "Counted by the model at the last reply; the categories are estimated.": "Contado por el modelo en la última respuesta; las categorías son estimaciones.",
"Cut short here; the whole of it is in the web interface.": "Recortado aquí; completo está en la interfaz web.", "Cut short here; the whole of it is in the web interface.": "Recortado aquí; completo está en la interfaz web.",
"Days": "Días", "Days": "Días",
@@ -753,6 +804,7 @@ const LEMBAS_I18N = {
"Delete this chat?": "¿Eliminar este chat?", "Delete this chat?": "¿Eliminar este chat?",
"Delete this scheduled task?": "¿Eliminar esta tarea programada?", "Delete this scheduled task?": "¿Eliminar esta tarea programada?",
"Deny": "Denegar", "Deny": "Denegar",
"Disconnected. Close and reopen to reconnect.": "Desconectado. Cierre y vuelva a abrir para reconectar.",
"Done": "Hecho", "Done": "Hecho",
"Drop the files to attach them": "Suelte los archivos para adjuntarlos", "Drop the files to attach them": "Suelte los archivos para adjuntarlos",
"Due %(when)s": "Prevista %(when)s", "Due %(when)s": "Prevista %(when)s",
@@ -827,12 +879,14 @@ const LEMBAS_I18N = {
"Not measured.": "Sin medir.", "Not measured.": "Sin medir.",
"Not run yet": "Aún no se ha ejecutado", "Not run yet": "Aún no se ha ejecutado",
"Not yet": "Todavía no", "Not yet": "Todavía no",
"Nothing has been run in this shell yet.": "Aún no se ha ejecutado nada en este shell.",
"Nothing has been said in the room yet.": "Todavía no se ha dicho nada en la sala.", "Nothing has been said in the room yet.": "Todavía no se ha dicho nada en la sala.",
"Nothing is scheduled.": "No hay nada programado.", "Nothing is scheduled.": "No hay nada programado.",
"Nothing matches.": "No hay coincidencias.", "Nothing matches.": "No hay coincidencias.",
"Nothing running.": "Nada en marcha.", "Nothing running.": "Nada en marcha.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "No hay nada en segundo plano. Los subagentes, los miembros de un equipo y los comandos en segundo plano aparecen aquí mientras trabajan.", "Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "No hay nada en segundo plano. Los subagentes, los miembros de un equipo y los comandos en segundo plano aparecen aquí mientras trabajan.",
"Nothing to report": "Nada que informar", "Nothing to report": "Nada que informar",
"Nothing to send: select some output first.": "Nada que enviar: seleccione primero algo de la salida.",
"Nothing yet.": "Aún nada.", "Nothing yet.": "Aún nada.",
"Notifications": "Notificaciones", "Notifications": "Notificaciones",
"One chat that grows": "Un chat que va creciendo", "One chat that grows": "Un chat que va creciendo",
@@ -854,9 +908,11 @@ const LEMBAS_I18N = {
"Plan — changes nothing": "Plan — no cambia nada", "Plan — changes nothing": "Plan — no cambia nada",
"Preview": "Vista previa", "Preview": "Vista previa",
"Project folders": "Carpetas de proyectos", "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",
"Read again": "Leer de nuevo", "Read again": "Leer de nuevo",
"Reasoning effort": "Esfuerzo de razonamiento", "Reasoning effort": "Esfuerzo de razonamiento",
"Recommended": "Recomendado", "Recommended": "Recomendado",
"Reconnecting…": "Reconectando…",
"Regenerate reply": "Regenerar respuesta", "Regenerate reply": "Regenerar respuesta",
"Remove": "Quitar", "Remove": "Quitar",
"Remove %(dir)s": "Quitar %(dir)s", "Remove %(dir)s": "Quitar %(dir)s",
@@ -882,6 +938,7 @@ const LEMBAS_I18N = {
"Search": "Buscar", "Search": "Buscar",
"Search chats": "Buscar en los chats", "Search chats": "Buscar en los chats",
"Send": "Enviar", "Send": "Enviar",
"Send to chat": "Enviar al chat",
"Sending…": "Enviando…", "Sending…": "Enviando…",
"Server": "Servidor", "Server": "Servidor",
"Set the time yourself": "Fijar la hora usted mismo", "Set the time yourself": "Fijar la hora usted mismo",
@@ -890,6 +947,7 @@ const LEMBAS_I18N = {
"Shown by the agent": "Mostrado por el agente", "Shown by the agent": "Mostrado por el agente",
"Sidebar": "Barra lateral", "Sidebar": "Barra lateral",
"Sign in": "Iniciar sesión", "Sign in": "Iniciar sesión",
"Sign in again: the server no longer accepts this app's sign-in.": "Vuelva a iniciar sesión: el servidor ya no acepta el inicio de sesión de esta aplicación.",
"Sign out": "Cerrar sesión", "Sign out": "Cerrar sesión",
"Signed in as %(who)s.": "Sesión iniciada como %(who)s.", "Signed in as %(who)s.": "Sesión iniciada como %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Al cerrar sesión, la aplicación renuncia a su acceso en el servidor; la vinculación del agente se mantiene. Cierre sesión para usar otro servidor.", "Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Al cerrar sesión, la aplicación renuncia a su acceso en el servidor; la vinculación del agente se mantiene. Cierre sesión para usar otro servidor.",
@@ -912,6 +970,7 @@ const LEMBAS_I18N = {
"Team: %(size)s of at most %(max)s subagents": "Equipo: %(size)s de un máximo de %(max)s subagentes", "Team: %(size)s of at most %(max)s subagents": "Equipo: %(size)s de un máximo de %(max)s subagentes",
"Tell me": "Avisarme", "Tell me": "Avisarme",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Avisarme cuando una respuesta o el trabajo de un agente termine mientras estoy en otra parte", "Tell me when a reply or an agent's work is finished while I am elsewhere": "Avisarme cuando una respuesta o el trabajo de un agente termine mientras estoy en otra parte",
"Terminal": "Terminal",
"The agent asks to run %(tool)s:": "El agente pide ejecutar %(tool)s:", "The agent asks to run %(tool)s:": "El agente pide ejecutar %(tool)s:",
"The agent asks:": "El agente pregunta:", "The agent asks:": "El agente pregunta:",
"The agent has finished.": "El agente ha terminado.", "The agent has finished.": "El agente ha terminado.",
@@ -931,11 +990,17 @@ const LEMBAS_I18N = {
"The last %(n)s runs failed: %(why)s": "Las últimas %(n)s ejecuciones fallaron: %(why)s", "The last %(n)s runs failed: %(why)s": "Las últimas %(n)s ejecuciones fallaron: %(why)s",
"The model's own": "La del modelo", "The model's own": "La del modelo",
"The most an agent chat may do here": "Lo máximo que un chat de agente puede hacer aquí", "The most an agent chat may do here": "Lo máximo que un chat de agente puede hacer aquí",
"The next command": "El comando siguiente",
"The next command (Ctrl+Shift+↓)": "El comando siguiente (Ctrl+Shift+↓)",
"The next runs": "Las próximas ejecuciones", "The next runs": "Las próximas ejecuciones",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Los números cuentan el agente propio del chat: 1 es el agente solo, sin subagentes; Máx. 4 es el agente y hasta 3 subagentes. Un subagente usa el modelo del chat, u otro solo si lo nombra en el chat.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Los números cuentan el agente propio del chat: 1 es el agente solo, sin subagentes; Máx. 4 es el agente y hasta 3 subagentes. Un subagente usa el modelo del chat, u otro solo si lo nombra en el chat.",
"The previous command": "El comando anterior",
"The previous command (Ctrl+Shift+↑)": "El comando anterior (Ctrl+Shift+↑)",
"The reply is ready.": "La respuesta está lista.", "The reply is ready.": "La respuesta está lista.",
"The result is no longer there.": "El resultado ya no está.", "The result is no longer there.": "El resultado ya no está.",
"The server refused this computer's link: %(why)s": "El servidor rechazó la vinculación de este equipo: %(why)s", "The server refused this computer's link: %(why)s": "El servidor rechazó la vinculación de este equipo: %(why)s",
"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.", "These words give no run in the future.": "Estas palabras no dan ninguna ejecución futura.",
"Think before answering": "Razonar antes de responder", "Think before answering": "Razonar antes de responder",
"Thinking": "Pensando", "Thinking": "Pensando",
@@ -948,6 +1013,7 @@ const LEMBAS_I18N = {
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Este modelo atiende una solicitud a la vez: los subagentes necesitan otro modelo, en otro servidor, nombrado en el chat.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Este modelo atiende una solicitud a la vez: los subagentes necesitan otro modelo, en otro servidor, nombrado en el chat.",
"This server cannot compact a chat from the app yet; the web interface can.": "Este servidor aún no puede compactar un chat desde la aplicación; la interfaz web sí puede.", "This server cannot compact a chat from the app yet; the web interface can.": "Este servidor aún no puede compactar un chat desde la aplicación; la interfaz web sí puede.",
"This server has no scheduled tasks yet.": "Este servidor aún no tiene tareas programadas.", "This server has no scheduled tasks yet.": "Este servidor aún no tiene tareas programadas.",
"This terminal closed.": "Este terminal se ha cerrado.",
"Thought": "Pensamiento", "Thought": "Pensamiento",
"Time": "Hora", "Time": "Hora",
"Times in %(zone)s": "Zona horaria: %(zone)s", "Times in %(zone)s": "Zona horaria: %(zone)s",
@@ -1061,6 +1127,7 @@ const LEMBAS_I18N = {
"Choose a model to choose its effort.": "Choisissez un modèle pour choisir son effort.", "Choose a model to choose its effort.": "Choisissez un modèle pour choisir son effort.",
"Close": "Fermer", "Close": "Fermer",
"Close (LLeMbas stays in the tray)": "Fermer (LLeMbas reste dans la zone de notification)", "Close (LLeMbas stays in the tray)": "Fermer (LLeMbas reste dans la zone de notification)",
"Close terminal": "Fermer le terminal",
"Close the activity panel": "Fermer le panneau d’activité", "Close the activity panel": "Fermer le panneau d’activité",
"Command": "Commande", "Command": "Commande",
"Commands": "Commandes", "Commands": "Commandes",
@@ -1070,12 +1137,18 @@ const LEMBAS_I18N = {
"Compacts before the next reply.": "Compacté avant la prochaine réponse.", "Compacts before the next reply.": "Compacté avant la prochaine réponse.",
"Compacts by itself once the window is known.": "Compacté de lui-même dès que la fenêtre est connue.", "Compacts by itself once the window is known.": "Compacté de lui-même dès que la fenêtre est connue.",
"Connect": "Se connecter", "Connect": "Se connecter",
"Connected.": "Connecté.",
"Connected. This shell is also open in another tab, and they share a size.": "Connecté. Ce shell est aussi ouvert dans un autre onglet, et ils partagent une taille.",
"Connecting…": "Connexion…",
"Context": "Contexte", "Context": "Contexte",
"Context and background tasks": "Contexte et tâches en arrière-plan", "Context and background tasks": "Contexte et tâches en arrière-plan",
"Continue in a terminal": "Continuer dans un terminal", "Continue in a terminal": "Continuer dans un terminal",
"Continue in a terminal…": "Continuer dans un terminal…", "Continue in a terminal…": "Continuer dans un terminal…",
"Copied": "Copié", "Copied": "Copié",
"Copied the last of the screen, as it appeared. This shell does not mark where commands begin.": "La fin de l'écran a été copiée, telle qu'elle apparaissait. Ce shell ne marque pas où commencent les commandes.",
"Copied.": "Copié.",
"Copy": "Copier", "Copy": "Copier",
"Copy the last command and its output": "Copier la dernière commande et sa sortie",
"Counted by the model at the last reply; the categories are estimated.": "Compté par le modèle lors de la dernière réponse ; les catégories sont estimées.", "Counted by the model at the last reply; the categories are estimated.": "Compté par le modèle lors de la dernière réponse ; les catégories sont estimées.",
"Cut short here; the whole of it is in the web interface.": "Tronqué ici ; le tout est dans l'interface web.", "Cut short here; the whole of it is in the web interface.": "Tronqué ici ; le tout est dans l'interface web.",
"Days": "Jours", "Days": "Jours",
@@ -1084,6 +1157,7 @@ const LEMBAS_I18N = {
"Delete this chat?": "Supprimer cette conversation ?", "Delete this chat?": "Supprimer cette conversation ?",
"Delete this scheduled task?": "Supprimer cette tâche planifiée ?", "Delete this scheduled task?": "Supprimer cette tâche planifiée ?",
"Deny": "Refuser", "Deny": "Refuser",
"Disconnected. Close and reopen to reconnect.": "Déconnecté. Fermez et rouvrez pour vous reconnecter.",
"Done": "Terminé", "Done": "Terminé",
"Drop the files to attach them": "Déposez les fichiers pour les joindre", "Drop the files to attach them": "Déposez les fichiers pour les joindre",
"Due %(when)s": "Prévue %(when)s", "Due %(when)s": "Prévue %(when)s",
@@ -1158,12 +1232,14 @@ const LEMBAS_I18N = {
"Not measured.": "Non mesuré.", "Not measured.": "Non mesuré.",
"Not run yet": "Pas encore exécutée", "Not run yet": "Pas encore exécutée",
"Not yet": "Pas encore", "Not yet": "Pas encore",
"Nothing has been run in this shell yet.": "Rien n'a encore été exécuté dans ce shell.",
"Nothing has been said in the room yet.": "Rien n’a encore été dit dans la salle.", "Nothing has been said in the room yet.": "Rien n’a encore été dit dans la salle.",
"Nothing is scheduled.": "Rien n’est planifié.", "Nothing is scheduled.": "Rien n’est planifié.",
"Nothing matches.": "Aucun résultat.", "Nothing matches.": "Aucun résultat.",
"Nothing running.": "Rien en cours.", "Nothing running.": "Rien en cours.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Rien ne tourne en arrière-plan. Les sous-agents, les membres d’une équipe et les commandes en arrière-plan s’affichent ici pendant qu’ils travaillent.", "Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Rien ne tourne en arrière-plan. Les sous-agents, les membres d’une équipe et les commandes en arrière-plan s’affichent ici pendant qu’ils travaillent.",
"Nothing to report": "Rien à signaler", "Nothing to report": "Rien à signaler",
"Nothing to send: select some output first.": "Rien à envoyer : sélectionnez d'abord une partie de la sortie.",
"Nothing yet.": "Rien pour l'instant.", "Nothing yet.": "Rien pour l'instant.",
"Notifications": "Notifications", "Notifications": "Notifications",
"One chat that grows": "Une conversation qui grandit", "One chat that grows": "Une conversation qui grandit",
@@ -1185,9 +1261,11 @@ const LEMBAS_I18N = {
"Plan — changes nothing": "Plan — ne change rien", "Plan — changes nothing": "Plan — ne change rien",
"Preview": "Aperçu", "Preview": "Aperçu",
"Project folders": "Dossiers de projets", "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",
"Read again": "Relire", "Read again": "Relire",
"Reasoning effort": "Effort de raisonnement", "Reasoning effort": "Effort de raisonnement",
"Recommended": "Recommandé", "Recommended": "Recommandé",
"Reconnecting…": "Reconnexion…",
"Regenerate reply": "Régénérer la réponse", "Regenerate reply": "Régénérer la réponse",
"Remove": "Retirer", "Remove": "Retirer",
"Remove %(dir)s": "Retirer %(dir)s", "Remove %(dir)s": "Retirer %(dir)s",
@@ -1213,6 +1291,7 @@ const LEMBAS_I18N = {
"Search": "Rechercher", "Search": "Rechercher",
"Search chats": "Rechercher dans les conversations", "Search chats": "Rechercher dans les conversations",
"Send": "Envoyer", "Send": "Envoyer",
"Send to chat": "Envoyer à la conversation",
"Sending…": "Envoi…", "Sending…": "Envoi…",
"Server": "Serveur", "Server": "Serveur",
"Set the time yourself": "Régler l'heure vous-même", "Set the time yourself": "Régler l'heure vous-même",
@@ -1221,6 +1300,7 @@ const LEMBAS_I18N = {
"Shown by the agent": "Montré par l'agent", "Shown by the agent": "Montré par l'agent",
"Sidebar": "Barre latérale", "Sidebar": "Barre latérale",
"Sign in": "Se connecter", "Sign in": "Se connecter",
"Sign in again: the server no longer accepts this app's sign-in.": "Reconnectez-vous : le serveur n’accepte plus la connexion de cette application.",
"Sign out": "Se déconnecter", "Sign out": "Se déconnecter",
"Signed in as %(who)s.": "Connecté en tant que %(who)s.", "Signed in as %(who)s.": "Connecté en tant que %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "La déconnexion abandonne la connexion de l'application au serveur ; la liaison de l'agent reste. Déconnectez-vous pour utiliser un autre serveur.", "Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "La déconnexion abandonne la connexion de l'application au serveur ; la liaison de l'agent reste. Déconnectez-vous pour utiliser un autre serveur.",
@@ -1243,6 +1323,7 @@ const LEMBAS_I18N = {
"Team: %(size)s of at most %(max)s subagents": "Équipe : %(size)s sur %(max)s sous-agents au plus", "Team: %(size)s of at most %(max)s subagents": "Équipe : %(size)s sur %(max)s sous-agents au plus",
"Tell me": "Me prévenir", "Tell me": "Me prévenir",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Me prévenir quand une réponse ou le travail d'un agent est terminé pendant que je suis ailleurs", "Tell me when a reply or an agent's work is finished while I am elsewhere": "Me prévenir quand une réponse ou le travail d'un agent est terminé pendant que je suis ailleurs",
"Terminal": "Terminal",
"The agent asks to run %(tool)s:": "L'agent demande à exécuter %(tool)s :", "The agent asks to run %(tool)s:": "L'agent demande à exécuter %(tool)s :",
"The agent asks:": "L'agent demande :", "The agent asks:": "L'agent demande :",
"The agent has finished.": "L'agent a terminé.", "The agent has finished.": "L'agent a terminé.",
@@ -1262,11 +1343,17 @@ const LEMBAS_I18N = {
"The last %(n)s runs failed: %(why)s": "Les %(n)s dernières exécutions ont échoué : %(why)s", "The last %(n)s runs failed: %(why)s": "Les %(n)s dernières exécutions ont échoué : %(why)s",
"The model's own": "Celle du modèle", "The model's own": "Celle du modèle",
"The most an agent chat may do here": "Le maximum qu'une conversation d'agent peut faire ici", "The most an agent chat may do here": "Le maximum qu'une conversation d'agent peut faire ici",
"The next command": "La commande suivante",
"The next command (Ctrl+Shift+↓)": "La commande suivante (Ctrl+Shift+↓)",
"The next runs": "Les prochaines exécutions", "The next runs": "Les prochaines exécutions",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Les nombres comptent l’agent de la conversation : 1, c’est l’agent seul, sans sous-agents ; Max 4, c’est l’agent et jusqu’à 3 sous-agents. Un sous-agent utilise le modèle de la conversation, ou un autre seulement si vous le nommez dans la conversation.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Les nombres comptent l’agent de la conversation : 1, c’est l’agent seul, sans sous-agents ; Max 4, c’est l’agent et jusqu’à 3 sous-agents. Un sous-agent utilise le modèle de la conversation, ou un autre seulement si vous le nommez dans la conversation.",
"The previous command": "La commande précédente",
"The previous command (Ctrl+Shift+↑)": "La commande précédente (Ctrl+Shift+↑)",
"The reply is ready.": "La réponse est prête.", "The reply is ready.": "La réponse est prête.",
"The result is no longer there.": "Le résultat n'est plus là.", "The result is no longer there.": "Le résultat n'est plus là.",
"The server refused this computer's link: %(why)s": "Le serveur a refusé la liaison de cet ordinateur : %(why)s", "The server refused this computer's link: %(why)s": "Le serveur a refusé la liaison de cet ordinateur : %(why)s",
"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.", "These words give no run in the future.": "Ces mots ne donnent aucune exécution à venir.",
"Think before answering": "Réfléchir avant de répondre", "Think before answering": "Réfléchir avant de répondre",
"Thinking": "Réflexion", "Thinking": "Réflexion",
@@ -1279,6 +1366,7 @@ const LEMBAS_I18N = {
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ce modèle traite une requête à la fois : les sous-agents ont besoin d’un autre modèle, sur un autre serveur, nommé dans la discussion.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ce modèle traite une requête à la fois : les sous-agents ont besoin d’un autre modèle, sur un autre serveur, nommé dans la discussion.",
"This server cannot compact a chat from the app yet; the web interface can.": "Ce serveur ne peut pas encore compacter une conversation depuis l’application ; l’interface web le peut.", "This server cannot compact a chat from the app yet; the web interface can.": "Ce serveur ne peut pas encore compacter une conversation depuis l’application ; l’interface web le peut.",
"This server has no scheduled tasks yet.": "Ce serveur n’a pas encore de tâches planifiées.", "This server has no scheduled tasks yet.": "Ce serveur n’a pas encore de tâches planifiées.",
"This terminal closed.": "Ce terminal s'est fermé.",
"Thought": "Réflexion", "Thought": "Réflexion",
"Time": "Heure", "Time": "Heure",
"Times in %(zone)s": "Fuseau horaire : %(zone)s", "Times in %(zone)s": "Fuseau horaire : %(zone)s",
@@ -1392,6 +1480,7 @@ const LEMBAS_I18N = {
"Choose a model to choose its effort.": "Válasszon modellt az erőfeszítés megadásához.", "Choose a model to choose its effort.": "Válasszon modellt az erőfeszítés megadásához.",
"Close": "Bezárás", "Close": "Bezárás",
"Close (LLeMbas stays in the tray)": "Bezárás (a LLeMbas a tálcán marad)", "Close (LLeMbas stays in the tray)": "Bezárás (a LLeMbas a tálcán marad)",
"Close terminal": "Terminál bezárása",
"Close the activity panel": "A tevékenységpanel bezárása", "Close the activity panel": "A tevékenységpanel bezárása",
"Command": "Parancs", "Command": "Parancs",
"Commands": "Parancsok", "Commands": "Parancsok",
@@ -1401,12 +1490,18 @@ const LEMBAS_I18N = {
"Compacts before the next reply.": "A következő válasz előtt tömörül.", "Compacts before the next reply.": "A következő válasz előtt tömörül.",
"Compacts by itself once the window is known.": "Magától tömörül, amint ismert az ablak.", "Compacts by itself once the window is known.": "Magától tömörül, amint ismert az ablak.",
"Connect": "Csatlakozás", "Connect": "Csatlakozás",
"Connected.": "Kapcsolódva.",
"Connected. This shell is also open in another tab, and they share a size.": "Kapcsolódva. Ez a shell egy másik lapon is nyitva van, és közös a méretük.",
"Connecting…": "Kapcsolódás…",
"Context": "Kontextus", "Context": "Kontextus",
"Context and background tasks": "Kontextus és háttérfeladatok", "Context and background tasks": "Kontextus és háttérfeladatok",
"Continue in a terminal": "Folytatás terminálban", "Continue in a terminal": "Folytatás terminálban",
"Continue in a terminal…": "Folytatás terminálban…", "Continue in a terminal…": "Folytatás terminálban…",
"Copied": "Másolva", "Copied": "Másolva",
"Copied the last of the screen, as it appeared. This shell does not mark where commands begin.": "A képernyő vége átmásolva, ahogy megjelent. Ez a shell nem jelöli, hol kezdődnek a parancsok.",
"Copied.": "Másolva.",
"Copy": "Másolás", "Copy": "Másolás",
"Copy the last command and its output": "Az utolsó parancs és kimenetének másolása",
"Counted by the model at the last reply; the categories are estimated.": "A modell számolta az utolsó válasznál; a kategóriák becslések.", "Counted by the model at the last reply; the categories are estimated.": "A modell számolta az utolsó válasznál; a kategóriák becslések.",
"Cut short here; the whole of it is in the web interface.": "Itt rövidítve; a teljes a webes felületen van.", "Cut short here; the whole of it is in the web interface.": "Itt rövidítve; a teljes a webes felületen van.",
"Days": "Napok", "Days": "Napok",
@@ -1415,6 +1510,7 @@ const LEMBAS_I18N = {
"Delete this chat?": "Törli ezt a csevegést?", "Delete this chat?": "Törli ezt a csevegést?",
"Delete this scheduled task?": "Törli ezt az ütemezett feladatot?", "Delete this scheduled task?": "Törli ezt az ütemezett feladatot?",
"Deny": "Elutasítás", "Deny": "Elutasítás",
"Disconnected. Close and reopen to reconnect.": "Kapcsolat bontva. Az újrakapcsolódáshoz zárja be és nyissa meg újra.",
"Done": "Kész", "Done": "Kész",
"Drop the files to attach them": "Engedje el a fájlokat a csatoláshoz", "Drop the files to attach them": "Engedje el a fájlokat a csatoláshoz",
"Due %(when)s": "Esedékes: %(when)s", "Due %(when)s": "Esedékes: %(when)s",
@@ -1489,12 +1585,14 @@ const LEMBAS_I18N = {
"Not measured.": "Nincs mérve.", "Not measured.": "Nincs mérve.",
"Not run yet": "Még nem futott", "Not run yet": "Még nem futott",
"Not yet": "Még nem", "Not yet": "Még nem",
"Nothing has been run in this shell yet.": "Ebben a shellben még semmi sem futott.",
"Nothing has been said in the room yet.": "A szobában még senki nem mondott semmit.", "Nothing has been said in the room yet.": "A szobában még senki nem mondott semmit.",
"Nothing is scheduled.": "Nincs semmi ütemezve.", "Nothing is scheduled.": "Nincs semmi ütemezve.",
"Nothing matches.": "Nincs találat.", "Nothing matches.": "Nincs találat.",
"Nothing running.": "Semmi sem fut.", "Nothing running.": "Semmi sem fut.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "A háttérben semmi sem fut. Az alügynökök, egy csapat tagjai és a háttérparancsok itt jelennek meg, amíg dolgoznak.", "Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "A háttérben semmi sem fut. Az alügynökök, egy csapat tagjai és a háttérparancsok itt jelennek meg, amíg dolgoznak.",
"Nothing to report": "Nincs jelentenivaló", "Nothing to report": "Nincs jelentenivaló",
"Nothing to send: select some output first.": "Nincs mit küldeni: előbb jelöljön ki valamennyi kimenetet.",
"Nothing yet.": "Még semmi.", "Nothing yet.": "Még semmi.",
"Notifications": "Értesítések", "Notifications": "Értesítések",
"One chat that grows": "Egy beszélgetés, amely bővül", "One chat that grows": "Egy beszélgetés, amely bővül",
@@ -1516,9 +1614,11 @@ const LEMBAS_I18N = {
"Plan — changes nothing": "Plan — semmit nem változtat", "Plan — changes nothing": "Plan — semmit nem változtat",
"Preview": "Előnézet", "Preview": "Előnézet",
"Project folders": "Projektmappák", "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",
"Read again": "Újraolvasás", "Read again": "Újraolvasás",
"Reasoning effort": "Gondolkodási erőfeszítés", "Reasoning effort": "Gondolkodási erőfeszítés",
"Recommended": "Ajánlott", "Recommended": "Ajánlott",
"Reconnecting…": "Újrakapcsolódás…",
"Regenerate reply": "Válasz újragenerálása", "Regenerate reply": "Válasz újragenerálása",
"Remove": "Eltávolítás", "Remove": "Eltávolítás",
"Remove %(dir)s": "%(dir)s eltávolítása", "Remove %(dir)s": "%(dir)s eltávolítása",
@@ -1544,6 +1644,7 @@ const LEMBAS_I18N = {
"Search": "Keresés", "Search": "Keresés",
"Search chats": "Keresés a beszélgetésekben", "Search chats": "Keresés a beszélgetésekben",
"Send": "Küldés", "Send": "Küldés",
"Send to chat": "Küldés a csevegésbe",
"Sending…": "Küldés…", "Sending…": "Küldés…",
"Server": "Kiszolgáló", "Server": "Kiszolgáló",
"Set the time yourself": "Az időpont megadása kézzel", "Set the time yourself": "Az időpont megadása kézzel",
@@ -1552,6 +1653,7 @@ const LEMBAS_I18N = {
"Shown by the agent": "Az ügynök mutatta", "Shown by the agent": "Az ügynök mutatta",
"Sidebar": "Oldalsáv", "Sidebar": "Oldalsáv",
"Sign in": "Bejelentkezés", "Sign in": "Bejelentkezés",
"Sign in again: the server no longer accepts this app's sign-in.": "Jelentkezzen be újra: a szerver már nem fogadja el ennek az alkalmazásnak a bejelentkezését.",
"Sign out": "Kijelentkezés", "Sign out": "Kijelentkezés",
"Signed in as %(who)s.": "Bejelentkezve: %(who)s.", "Signed in as %(who)s.": "Bejelentkezve: %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Kijelentkezéskor az alkalmazás lemond a szerveren lévő bejelentkezéséről; az ügynök kapcsolata megmarad. Más szerver használatához jelentkezzen ki.", "Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Kijelentkezéskor az alkalmazás lemond a szerveren lévő bejelentkezéséről; az ügynök kapcsolata megmarad. Más szerver használatához jelentkezzen ki.",
@@ -1574,6 +1676,7 @@ const LEMBAS_I18N = {
"Team: %(size)s of at most %(max)s subagents": "Csapat: %(size)s a legfeljebb %(max)s alügynökből", "Team: %(size)s of at most %(max)s subagents": "Csapat: %(size)s a legfeljebb %(max)s alügynökből",
"Tell me": "Értesítés", "Tell me": "Értesítés",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Szóljon, ha egy válasz vagy az ügynök munkája elkészül, amíg máshol vagyok", "Tell me when a reply or an agent's work is finished while I am elsewhere": "Szóljon, ha egy válasz vagy az ügynök munkája elkészül, amíg máshol vagyok",
"Terminal": "Terminál",
"The agent asks to run %(tool)s:": "Az ügynök a(z) %(tool)s futtatását kéri:", "The agent asks to run %(tool)s:": "Az ügynök a(z) %(tool)s futtatását kéri:",
"The agent asks:": "Az ügynök kérdezi:", "The agent asks:": "Az ügynök kérdezi:",
"The agent has finished.": "Az ügynök befejezte.", "The agent has finished.": "Az ügynök befejezte.",
@@ -1593,11 +1696,17 @@ const LEMBAS_I18N = {
"The last %(n)s runs failed: %(why)s": "Az utolsó %(n)s futás sikertelen: %(why)s", "The last %(n)s runs failed: %(why)s": "Az utolsó %(n)s futás sikertelen: %(why)s",
"The model's own": "A modell sajátja", "The model's own": "A modell sajátja",
"The most an agent chat may do here": "A legtöbb, amit egy ügynök-csevegés itt tehet", "The most an agent chat may do here": "A legtöbb, amit egy ügynök-csevegés itt tehet",
"The next command": "Következő parancs",
"The next command (Ctrl+Shift+↓)": "Következő parancs (Ctrl+Shift+↓)",
"The next runs": "A következő futások", "The next runs": "A következő futások",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "A számokba a beszélgetés saját ügynöke is beleszámít: az 1 maga az ügynök, alügynökök nélkül; a Max. 4 az ügynök és legfeljebb 3 alügynök. Az alügynök a beszélgetés modelljén fut, másikon csak akkor, ha megnevezi a beszélgetésben.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "A számokba a beszélgetés saját ügynöke is beleszámít: az 1 maga az ügynök, alügynökök nélkül; a Max. 4 az ügynök és legfeljebb 3 alügynök. Az alügynök a beszélgetés modelljén fut, másikon csak akkor, ha megnevezi a beszélgetésben.",
"The previous command": "Előző parancs",
"The previous command (Ctrl+Shift+↑)": "Előző parancs (Ctrl+Shift+↑)",
"The reply is ready.": "A válasz kész.", "The reply is ready.": "A válasz kész.",
"The result is no longer there.": "Az eredmény már nincs meg.", "The result is no longer there.": "Az eredmény már nincs meg.",
"The server refused this computer's link: %(why)s": "A szerver elutasította ennek a számítógépnek a kapcsolatát: %(why)s", "The server refused this computer's link: %(why)s": "A szerver elutasította ennek a számítógépnek a kapcsolatát: %(why)s",
"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.", "These words give no run in the future.": "Ezekből a szavakból nem következik jövőbeli futás.",
"Think before answering": "Gondolkodjon válasz előtt", "Think before answering": "Gondolkodjon válasz előtt",
"Thinking": "Gondolkodás", "Thinking": "Gondolkodás",
@@ -1610,6 +1719,7 @@ const LEMBAS_I18N = {
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ez a modell egyszerre egy kérést szolgál ki: az alügynököknek másik modell kell, egy másik szerveren, a beszélgetésben megnevezve.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ez a modell egyszerre egy kérést szolgál ki: az alügynököknek másik modell kell, egy másik szerveren, a beszélgetésben megnevezve.",
"This server cannot compact a chat from the app yet; the web interface can.": "Ez a szerver még nem tud beszélgetést tömöríteni az alkalmazásból; a webes felület tud.", "This server cannot compact a chat from the app yet; the web interface can.": "Ez a szerver még nem tud beszélgetést tömöríteni az alkalmazásból; a webes felület tud.",
"This server has no scheduled tasks yet.": "Ezen a szerveren még nincsenek ütemezett feladatok.", "This server has no scheduled tasks yet.": "Ezen a szerveren még nincsenek ütemezett feladatok.",
"This terminal closed.": "Ez a terminál bezárult.",
"Thought": "Gondolkodott", "Thought": "Gondolkodott",
"Time": "Időpont", "Time": "Időpont",
"Times in %(zone)s": "Időzóna: %(zone)s", "Times in %(zone)s": "Időzóna: %(zone)s",
@@ -1723,6 +1833,7 @@ const LEMBAS_I18N = {
"Choose a model to choose its effort.": "Scegli un modello per sceglierne l’impegno.", "Choose a model to choose its effort.": "Scegli un modello per sceglierne l’impegno.",
"Close": "Chiudi", "Close": "Chiudi",
"Close (LLeMbas stays in the tray)": "Chiudi (LLeMbas resta nell'area di notifica)", "Close (LLeMbas stays in the tray)": "Chiudi (LLeMbas resta nell'area di notifica)",
"Close terminal": "Chiudi terminale",
"Close the activity panel": "Chiudi il pannello attività", "Close the activity panel": "Chiudi il pannello attività",
"Command": "Comando", "Command": "Comando",
"Commands": "Comandi", "Commands": "Comandi",
@@ -1732,12 +1843,18 @@ const LEMBAS_I18N = {
"Compacts before the next reply.": "Si compatta prima della prossima risposta.", "Compacts before the next reply.": "Si compatta prima della prossima risposta.",
"Compacts by itself once the window is known.": "Si compatta da sola appena la finestra è nota.", "Compacts by itself once the window is known.": "Si compatta da sola appena la finestra è nota.",
"Connect": "Connetti", "Connect": "Connetti",
"Connected.": "Connesso.",
"Connected. This shell is also open in another tab, and they share a size.": "Connesso. Questa shell è aperta anche in un'altra scheda, e le due condividono la dimensione.",
"Connecting…": "Connessione in corso…",
"Context": "Contesto", "Context": "Contesto",
"Context and background tasks": "Contesto e attività in background", "Context and background tasks": "Contesto e attività in background",
"Continue in a terminal": "Continua in un terminale", "Continue in a terminal": "Continua in un terminale",
"Continue in a terminal…": "Continua in un terminale…", "Continue in a terminal…": "Continua in un terminale…",
"Copied": "Copiato", "Copied": "Copiato",
"Copied the last of the screen, as it appeared. This shell does not mark where commands begin.": "Copiata l'ultima parte dello schermo, così come appariva. Questa shell non segna dove iniziano i comandi.",
"Copied.": "Copiato.",
"Copy": "Copia", "Copy": "Copia",
"Copy the last command and its output": "Copia l'ultimo comando e il suo output",
"Counted by the model at the last reply; the categories are estimated.": "Contato dal modello all’ultima risposta; le categorie sono stime.", "Counted by the model at the last reply; the categories are estimated.": "Contato dal modello all’ultima risposta; le categorie sono stime.",
"Cut short here; the whole of it is in the web interface.": "Troncato qui; il resto è nell'interfaccia web.", "Cut short here; the whole of it is in the web interface.": "Troncato qui; il resto è nell'interfaccia web.",
"Days": "Giorni", "Days": "Giorni",
@@ -1746,6 +1863,7 @@ const LEMBAS_I18N = {
"Delete this chat?": "Eliminare questa chat?", "Delete this chat?": "Eliminare questa chat?",
"Delete this scheduled task?": "Eliminare questa attività pianificata?", "Delete this scheduled task?": "Eliminare questa attività pianificata?",
"Deny": "Nega", "Deny": "Nega",
"Disconnected. Close and reopen to reconnect.": "Disconnesso. Chiudi e riapri per riconnetterti.",
"Done": "Fatto", "Done": "Fatto",
"Drop the files to attach them": "Rilasci i file per allegarli", "Drop the files to attach them": "Rilasci i file per allegarli",
"Due %(when)s": "Prevista %(when)s", "Due %(when)s": "Prevista %(when)s",
@@ -1820,12 +1938,14 @@ const LEMBAS_I18N = {
"Not measured.": "Non misurato.", "Not measured.": "Non misurato.",
"Not run yet": "Non ancora eseguita", "Not run yet": "Non ancora eseguita",
"Not yet": "Non ancora", "Not yet": "Non ancora",
"Nothing has been run in this shell yet.": "In questa shell non è ancora stato eseguito nulla.",
"Nothing has been said in the room yet.": "Nella stanza non è ancora stato detto nulla.", "Nothing has been said in the room yet.": "Nella stanza non è ancora stato detto nulla.",
"Nothing is scheduled.": "Non c’è niente di pianificato.", "Nothing is scheduled.": "Non c’è niente di pianificato.",
"Nothing matches.": "Nessun risultato.", "Nothing matches.": "Nessun risultato.",
"Nothing running.": "Nulla in esecuzione.", "Nothing running.": "Nulla in esecuzione.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Nulla è in esecuzione in background. Sottoagenti, membri di una squadra e comandi in background compaiono qui mentre lavorano.", "Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Nulla è in esecuzione in background. Sottoagenti, membri di una squadra e comandi in background compaiono qui mentre lavorano.",
"Nothing to report": "Niente da segnalare", "Nothing to report": "Niente da segnalare",
"Nothing to send: select some output first.": "Niente da inviare: seleziona prima un po' di output.",
"Nothing yet.": "Ancora niente.", "Nothing yet.": "Ancora niente.",
"Notifications": "Notifiche", "Notifications": "Notifiche",
"One chat that grows": "Una chat che cresce", "One chat that grows": "Una chat che cresce",
@@ -1847,9 +1967,11 @@ const LEMBAS_I18N = {
"Plan — changes nothing": "Plan — non cambia nulla", "Plan — changes nothing": "Plan — non cambia nulla",
"Preview": "Anteprima", "Preview": "Anteprima",
"Project folders": "Cartelle dei progetti", "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",
"Read again": "Rileggi", "Read again": "Rileggi",
"Reasoning effort": "Sforzo di ragionamento", "Reasoning effort": "Sforzo di ragionamento",
"Recommended": "Consigliato", "Recommended": "Consigliato",
"Reconnecting…": "Riconnessione…",
"Regenerate reply": "Rigenera risposta", "Regenerate reply": "Rigenera risposta",
"Remove": "Rimuovi", "Remove": "Rimuovi",
"Remove %(dir)s": "Rimuovi %(dir)s", "Remove %(dir)s": "Rimuovi %(dir)s",
@@ -1875,6 +1997,7 @@ const LEMBAS_I18N = {
"Search": "Cerca", "Search": "Cerca",
"Search chats": "Cerca nelle chat", "Search chats": "Cerca nelle chat",
"Send": "Invia", "Send": "Invia",
"Send to chat": "Invia alla chat",
"Sending…": "Invio…", "Sending…": "Invio…",
"Server": "Server", "Server": "Server",
"Set the time yourself": "Imposta tu l’orario", "Set the time yourself": "Imposta tu l’orario",
@@ -1883,6 +2006,7 @@ const LEMBAS_I18N = {
"Shown by the agent": "Mostrato dall'agente", "Shown by the agent": "Mostrato dall'agente",
"Sidebar": "Barra laterale", "Sidebar": "Barra laterale",
"Sign in": "Accedi", "Sign in": "Accedi",
"Sign in again: the server no longer accepts this app's sign-in.": "Accedi di nuovo: il server non accetta più l’accesso di questa app.",
"Sign out": "Esci", "Sign out": "Esci",
"Signed in as %(who)s.": "Accesso effettuato come %(who)s.", "Signed in as %(who)s.": "Accesso effettuato come %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Uscendo, l'app rinuncia al suo accesso sul server; il collegamento dell'agente resta. Esca per usare un altro server.", "Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Uscendo, l'app rinuncia al suo accesso sul server; il collegamento dell'agente resta. Esca per usare un altro server.",
@@ -1905,6 +2029,7 @@ const LEMBAS_I18N = {
"Team: %(size)s of at most %(max)s subagents": "Squadra: %(size)s su un massimo di %(max)s sottoagenti", "Team: %(size)s of at most %(max)s subagents": "Squadra: %(size)s su un massimo di %(max)s sottoagenti",
"Tell me": "Avvisami", "Tell me": "Avvisami",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Avvisami quando una risposta o il lavoro di un agente è finito mentre sono altrove", "Tell me when a reply or an agent's work is finished while I am elsewhere": "Avvisami quando una risposta o il lavoro di un agente è finito mentre sono altrove",
"Terminal": "Terminale",
"The agent asks to run %(tool)s:": "L'agente chiede di eseguire %(tool)s:", "The agent asks to run %(tool)s:": "L'agente chiede di eseguire %(tool)s:",
"The agent asks:": "L'agente chiede:", "The agent asks:": "L'agente chiede:",
"The agent has finished.": "L'agente ha finito.", "The agent has finished.": "L'agente ha finito.",
@@ -1924,11 +2049,17 @@ const LEMBAS_I18N = {
"The last %(n)s runs failed: %(why)s": "Le ultime %(n)s esecuzioni non sono riuscite: %(why)s", "The last %(n)s runs failed: %(why)s": "Le ultime %(n)s esecuzioni non sono riuscite: %(why)s",
"The model's own": "Quello del modello", "The model's own": "Quello del modello",
"The most an agent chat may do here": "Il massimo che una chat agente può fare qui", "The most an agent chat may do here": "Il massimo che una chat agente può fare qui",
"The next command": "Il comando successivo",
"The next command (Ctrl+Shift+↓)": "Il comando successivo (Ctrl+Shift+↓)",
"The next runs": "Le prossime esecuzioni", "The next runs": "Le prossime esecuzioni",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "I numeri contano anche l’agente della chat: 1 è l’agente da solo, senza sottoagenti; Max 4 è l’agente più fino a 3 sottoagenti. Un sottoagente usa il modello della chat, o un altro solo se lo nomini nella chat.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "I numeri contano anche l’agente della chat: 1 è l’agente da solo, senza sottoagenti; Max 4 è l’agente più fino a 3 sottoagenti. Un sottoagente usa il modello della chat, o un altro solo se lo nomini nella chat.",
"The previous command": "Il comando precedente",
"The previous command (Ctrl+Shift+↑)": "Il comando precedente (Ctrl+Shift+↑)",
"The reply is ready.": "La risposta è pronta.", "The reply is ready.": "La risposta è pronta.",
"The result is no longer there.": "Il risultato non c’è più.", "The result is no longer there.": "Il risultato non c’è più.",
"The server refused this computer's link: %(why)s": "Il server ha rifiutato il collegamento di questo computer: %(why)s", "The server refused this computer's link: %(why)s": "Il server ha rifiutato il collegamento di questo computer: %(why)s",
"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.", "These words give no run in the future.": "Queste parole non danno nessuna esecuzione futura.",
"Think before answering": "Ragiona prima di rispondere", "Think before answering": "Ragiona prima di rispondere",
"Thinking": "Ragionamento", "Thinking": "Ragionamento",
@@ -1941,6 +2072,7 @@ const LEMBAS_I18N = {
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Questo modello serve una richiesta alla volta: i sottoagenti hanno bisogno di un altro modello, su un altro server, nominato nella chat.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Questo modello serve una richiesta alla volta: i sottoagenti hanno bisogno di un altro modello, su un altro server, nominato nella chat.",
"This server cannot compact a chat from the app yet; the web interface can.": "Questo server non può ancora compattare una chat dall’app; l’interfaccia web sì.", "This server cannot compact a chat from the app yet; the web interface can.": "Questo server non può ancora compattare una chat dall’app; l’interfaccia web sì.",
"This server has no scheduled tasks yet.": "Questo server non ha ancora attività pianificate.", "This server has no scheduled tasks yet.": "Questo server non ha ancora attività pianificate.",
"This terminal closed.": "Questo terminale si è chiuso.",
"Thought": "Ha ragionato", "Thought": "Ha ragionato",
"Time": "Ora", "Time": "Ora",
"Times in %(zone)s": "Fuso orario: %(zone)s", "Times in %(zone)s": "Fuso orario: %(zone)s",
@@ -2054,6 +2186,7 @@ const LEMBAS_I18N = {
"Choose a model to choose its effort.": "Wybierz model, aby wybrać jego wysiłek.", "Choose a model to choose its effort.": "Wybierz model, aby wybrać jego wysiłek.",
"Close": "Zamknij", "Close": "Zamknij",
"Close (LLeMbas stays in the tray)": "Zamknij (LLeMbas zostaje w zasobniku systemowym)", "Close (LLeMbas stays in the tray)": "Zamknij (LLeMbas zostaje w zasobniku systemowym)",
"Close terminal": "Zamknij terminal",
"Close the activity panel": "Zamknij panel aktywności", "Close the activity panel": "Zamknij panel aktywności",
"Command": "Polecenie", "Command": "Polecenie",
"Commands": "Polecenia", "Commands": "Polecenia",
@@ -2063,12 +2196,18 @@ const LEMBAS_I18N = {
"Compacts before the next reply.": "Zostanie streszczona przed następną odpowiedzią.", "Compacts before the next reply.": "Zostanie streszczona przed następną odpowiedzią.",
"Compacts by itself once the window is known.": "Zostanie streszczona sama, gdy okno będzie znane.", "Compacts by itself once the window is known.": "Zostanie streszczona sama, gdy okno będzie znane.",
"Connect": "Połącz", "Connect": "Połącz",
"Connected.": "Połączono.",
"Connected. This shell is also open in another tab, and they share a size.": "Połączono. Ta powłoka jest otwarta też w innej karcie i dzielą one rozmiar.",
"Connecting…": "Łączenie…",
"Context": "Kontekst", "Context": "Kontekst",
"Context and background tasks": "Kontekst i zadania w tle", "Context and background tasks": "Kontekst i zadania w tle",
"Continue in a terminal": "Kontynuuj w terminalu", "Continue in a terminal": "Kontynuuj w terminalu",
"Continue in a terminal…": "Kontynuuj w terminalu…", "Continue in a terminal…": "Kontynuuj w terminalu…",
"Copied": "Skopiowano", "Copied": "Skopiowano",
"Copied the last of the screen, as it appeared. This shell does not mark where commands begin.": "Skopiowano koniec ekranu tak, jak wyglądał. Ta powłoka nie oznacza, gdzie zaczynają się polecenia.",
"Copied.": "Skopiowano.",
"Copy": "Kopiuj", "Copy": "Kopiuj",
"Copy the last command and its output": "Kopiuj ostatnie polecenie i jego wynik",
"Counted by the model at the last reply; the categories are estimated.": "Policzone przez model przy ostatniej odpowiedzi; kategorie są szacunkowe.", "Counted by the model at the last reply; the categories are estimated.": "Policzone przez model przy ostatniej odpowiedzi; kategorie są szacunkowe.",
"Cut short here; the whole of it is in the web interface.": "Tu skrócone; całość jest w interfejsie WWW.", "Cut short here; the whole of it is in the web interface.": "Tu skrócone; całość jest w interfejsie WWW.",
"Days": "Dni", "Days": "Dni",
@@ -2077,6 +2216,7 @@ const LEMBAS_I18N = {
"Delete this chat?": "Usunąć ten czat?", "Delete this chat?": "Usunąć ten czat?",
"Delete this scheduled task?": "Usunąć to zaplanowane zadanie?", "Delete this scheduled task?": "Usunąć to zaplanowane zadanie?",
"Deny": "Odrzuć", "Deny": "Odrzuć",
"Disconnected. Close and reopen to reconnect.": "Rozłączono. Aby połączyć ponownie, zamknij i otwórz ponownie.",
"Done": "Gotowe", "Done": "Gotowe",
"Drop the files to attach them": "Upuść pliki, aby je dołączyć", "Drop the files to attach them": "Upuść pliki, aby je dołączyć",
"Due %(when)s": "Zaplanowane na %(when)s", "Due %(when)s": "Zaplanowane na %(when)s",
@@ -2151,12 +2291,14 @@ const LEMBAS_I18N = {
"Not measured.": "Nie zmierzono.", "Not measured.": "Nie zmierzono.",
"Not run yet": "Jeszcze nie uruchomione", "Not run yet": "Jeszcze nie uruchomione",
"Not yet": "Jeszcze nie", "Not yet": "Jeszcze nie",
"Nothing has been run in this shell yet.": "W tej powłoce nic jeszcze nie uruchomiono.",
"Nothing has been said in the room yet.": "W pokoju nikt jeszcze nic nie powiedział.", "Nothing has been said in the room yet.": "W pokoju nikt jeszcze nic nie powiedział.",
"Nothing is scheduled.": "Nic nie jest zaplanowane.", "Nothing is scheduled.": "Nic nie jest zaplanowane.",
"Nothing matches.": "Nic nie pasuje.", "Nothing matches.": "Nic nie pasuje.",
"Nothing running.": "Nic nie działa.", "Nothing running.": "Nic nie działa.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "W tle nic nie działa. Podagenci, członkowie zespołu i polecenia w tle pojawiają się tu, gdy pracują.", "Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "W tle nic nie działa. Podagenci, członkowie zespołu i polecenia w tle pojawiają się tu, gdy pracują.",
"Nothing to report": "Nic do zgłoszenia", "Nothing to report": "Nic do zgłoszenia",
"Nothing to send: select some output first.": "Nie ma czego wysłać: najpierw należy zaznaczyć jakiś wynik.",
"Nothing yet.": "Jeszcze nic.", "Nothing yet.": "Jeszcze nic.",
"Notifications": "Powiadomienia", "Notifications": "Powiadomienia",
"One chat that grows": "Jeden czat, który rośnie", "One chat that grows": "Jeden czat, który rośnie",
@@ -2178,9 +2320,11 @@ const LEMBAS_I18N = {
"Plan — changes nothing": "Plan — niczego nie zmienia", "Plan — changes nothing": "Plan — niczego nie zmienia",
"Preview": "Podgląd", "Preview": "Podgląd",
"Project folders": "Foldery projektów", "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",
"Read again": "Wczytaj ponownie", "Read again": "Wczytaj ponownie",
"Reasoning effort": "Poziom rozumowania", "Reasoning effort": "Poziom rozumowania",
"Recommended": "Zalecane", "Recommended": "Zalecane",
"Reconnecting…": "Ponowne łączenie…",
"Regenerate reply": "Wygeneruj odpowiedź ponownie", "Regenerate reply": "Wygeneruj odpowiedź ponownie",
"Remove": "Usuń", "Remove": "Usuń",
"Remove %(dir)s": "Usuń %(dir)s", "Remove %(dir)s": "Usuń %(dir)s",
@@ -2206,6 +2350,7 @@ const LEMBAS_I18N = {
"Search": "Szukaj", "Search": "Szukaj",
"Search chats": "Szukaj w czatach", "Search chats": "Szukaj w czatach",
"Send": "Wyślij", "Send": "Wyślij",
"Send to chat": "Wyślij do czatu",
"Sending…": "Wysyłanie…", "Sending…": "Wysyłanie…",
"Server": "Serwer", "Server": "Serwer",
"Set the time yourself": "Ustaw czas samodzielnie", "Set the time yourself": "Ustaw czas samodzielnie",
@@ -2214,6 +2359,7 @@ const LEMBAS_I18N = {
"Shown by the agent": "Pokazane przez agenta", "Shown by the agent": "Pokazane przez agenta",
"Sidebar": "Pasek boczny", "Sidebar": "Pasek boczny",
"Sign in": "Zaloguj się", "Sign in": "Zaloguj się",
"Sign in again: the server no longer accepts this app's sign-in.": "Zaloguj się ponownie: serwer nie przyjmuje już logowania tej aplikacji.",
"Sign out": "Wyloguj się", "Sign out": "Wyloguj się",
"Signed in as %(who)s.": "Zalogowano jako %(who)s.", "Signed in as %(who)s.": "Zalogowano jako %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Wylogowanie zrzeka się logowania aplikacji na serwerze; połączenie agenta zostaje. Wyloguj się, aby użyć innego serwera.", "Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Wylogowanie zrzeka się logowania aplikacji na serwerze; połączenie agenta zostaje. Wyloguj się, aby użyć innego serwera.",
@@ -2236,6 +2382,7 @@ const LEMBAS_I18N = {
"Team: %(size)s of at most %(max)s subagents": "Zespół: %(size)s z najwyżej %(max)s podagentów", "Team: %(size)s of at most %(max)s subagents": "Zespół: %(size)s z najwyżej %(max)s podagentów",
"Tell me": "Powiadamiaj", "Tell me": "Powiadamiaj",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Powiadom mnie, gdy odpowiedź lub praca agenta jest gotowa, a ja jestem gdzie indziej", "Tell me when a reply or an agent's work is finished while I am elsewhere": "Powiadom mnie, gdy odpowiedź lub praca agenta jest gotowa, a ja jestem gdzie indziej",
"Terminal": "Terminal",
"The agent asks to run %(tool)s:": "Agent prosi o uruchomienie %(tool)s:", "The agent asks to run %(tool)s:": "Agent prosi o uruchomienie %(tool)s:",
"The agent asks:": "Agent pyta:", "The agent asks:": "Agent pyta:",
"The agent has finished.": "Agent skończył.", "The agent has finished.": "Agent skończył.",
@@ -2255,11 +2402,17 @@ const LEMBAS_I18N = {
"The last %(n)s runs failed: %(why)s": "Ostatnie uruchomienia nie powiodły się (%(n)s): %(why)s", "The last %(n)s runs failed: %(why)s": "Ostatnie uruchomienia nie powiodły się (%(n)s): %(why)s",
"The model's own": "Domyślne modelu", "The model's own": "Domyślne modelu",
"The most an agent chat may do here": "Najwięcej, co czat agenta może tu zrobić", "The most an agent chat may do here": "Najwięcej, co czat agenta może tu zrobić",
"The next command": "Następne polecenie",
"The next command (Ctrl+Shift+↓)": "Następne polecenie (Ctrl+Shift+↓)",
"The next runs": "Następne uruchomienia", "The next runs": "Następne uruchomienia",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Liczby obejmują własnego agenta czatu: 1 to sam agent, bez podagentów; Maks 4 to agent i do 3 podagentów. Podagent działa na modelu czatu, na innym tylko wtedy, gdy wskażesz go w czacie.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Liczby obejmują własnego agenta czatu: 1 to sam agent, bez podagentów; Maks 4 to agent i do 3 podagentów. Podagent działa na modelu czatu, na innym tylko wtedy, gdy wskażesz go w czacie.",
"The previous command": "Poprzednie polecenie",
"The previous command (Ctrl+Shift+↑)": "Poprzednie polecenie (Ctrl+Shift+↑)",
"The reply is ready.": "Odpowiedź jest gotowa.", "The reply is ready.": "Odpowiedź jest gotowa.",
"The result is no longer there.": "Wyniku już nie ma.", "The result is no longer there.": "Wyniku już nie ma.",
"The server refused this computer's link: %(why)s": "Serwer odrzucił połączenie tego komputera: %(why)s", "The server refused this computer's link: %(why)s": "Serwer odrzucił połączenie tego komputera: %(why)s",
"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.", "These words give no run in the future.": "Z tych słów nie wynika żadne przyszłe uruchomienie.",
"Think before answering": "Rozumuj przed odpowiedzią", "Think before answering": "Rozumuj przed odpowiedzią",
"Thinking": "Rozumowanie", "Thinking": "Rozumowanie",
@@ -2272,6 +2425,7 @@ const LEMBAS_I18N = {
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ten model obsługuje jedno żądanie naraz: podagenci potrzebują innego modelu, na innym serwerze, nazwanego w czacie.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Ten model obsługuje jedno żądanie naraz: podagenci potrzebują innego modelu, na innym serwerze, nazwanego w czacie.",
"This server cannot compact a chat from the app yet; the web interface can.": "Ten serwer nie umie jeszcze streścić czatu z aplikacji; interfejs WWW umie.", "This server cannot compact a chat from the app yet; the web interface can.": "Ten serwer nie umie jeszcze streścić czatu z aplikacji; interfejs WWW umie.",
"This server has no scheduled tasks yet.": "Ten serwer nie ma jeszcze zaplanowanych zadań.", "This server has no scheduled tasks yet.": "Ten serwer nie ma jeszcze zaplanowanych zadań.",
"This terminal closed.": "Ten terminal został zamknięty.",
"Thought": "Rozumowanie", "Thought": "Rozumowanie",
"Time": "Godzina", "Time": "Godzina",
"Times in %(zone)s": "Strefa czasowa: %(zone)s", "Times in %(zone)s": "Strefa czasowa: %(zone)s",
@@ -2385,6 +2539,7 @@ const LEMBAS_I18N = {
"Choose a model to choose its effort.": "Vyberte model, aby ste mohli zvoliť jeho náročnosť.", "Choose a model to choose its effort.": "Vyberte model, aby ste mohli zvoliť jeho náročnosť.",
"Close": "Zavrieť", "Close": "Zavrieť",
"Close (LLeMbas stays in the tray)": "Zavrieť (LLeMbas zostane v oznamovacej oblasti)", "Close (LLeMbas stays in the tray)": "Zavrieť (LLeMbas zostane v oznamovacej oblasti)",
"Close terminal": "Zavrieť terminál",
"Close the activity panel": "Zavrieť panel aktivity", "Close the activity panel": "Zavrieť panel aktivity",
"Command": "Príkaz", "Command": "Príkaz",
"Commands": "Príkazy", "Commands": "Príkazy",
@@ -2394,12 +2549,18 @@ const LEMBAS_I18N = {
"Compacts before the next reply.": "Zhrnie sa pred ďalšou odpoveďou.", "Compacts before the next reply.": "Zhrnie sa pred ďalšou odpoveďou.",
"Compacts by itself once the window is known.": "Zhrnie sa samo, keď bude okno známe.", "Compacts by itself once the window is known.": "Zhrnie sa samo, keď bude okno známe.",
"Connect": "Pripojiť", "Connect": "Pripojiť",
"Connected.": "Pripojené.",
"Connected. This shell is also open in another tab, and they share a size.": "Pripojené. Tento shell je otvorený aj v inej karte a zdieľajú veľkosť.",
"Connecting…": "Pripája sa…",
"Context": "Kontext", "Context": "Kontext",
"Context and background tasks": "Kontext a úlohy na pozadí", "Context and background tasks": "Kontext a úlohy na pozadí",
"Continue in a terminal": "Pokračovať v termináli", "Continue in a terminal": "Pokračovať v termináli",
"Continue in a terminal…": "Pokračovať v termináli…", "Continue in a terminal…": "Pokračovať v termináli…",
"Copied": "Skopírované", "Copied": "Skopírované",
"Copied the last of the screen, as it appeared. This shell does not mark where commands begin.": "Skopírovaný koniec obrazovky, tak ako vyzeral. Tento shell neoznačuje, kde sa príkazy začínajú.",
"Copied.": "Skopírované.",
"Copy": "Kopírovať", "Copy": "Kopírovať",
"Copy the last command and its output": "Skopírovať posledný príkaz a jeho výstup",
"Counted by the model at the last reply; the categories are estimated.": "Spočítal model pri poslednej odpovedi; kategórie sú odhadnuté.", "Counted by the model at the last reply; the categories are estimated.": "Spočítal model pri poslednej odpovedi; kategórie sú odhadnuté.",
"Cut short here; the whole of it is in the web interface.": "Tu skrátené; celé je to vo webovom rozhraní.", "Cut short here; the whole of it is in the web interface.": "Tu skrátené; celé je to vo webovom rozhraní.",
"Days": "Dni", "Days": "Dni",
@@ -2408,6 +2569,7 @@ const LEMBAS_I18N = {
"Delete this chat?": "Zmazať túto konverzáciu?", "Delete this chat?": "Zmazať túto konverzáciu?",
"Delete this scheduled task?": "Odstrániť túto naplánovanú úlohu?", "Delete this scheduled task?": "Odstrániť túto naplánovanú úlohu?",
"Deny": "Odmietnuť", "Deny": "Odmietnuť",
"Disconnected. Close and reopen to reconnect.": "Odpojené. Zavrite a znova otvorte, aby sa pripojilo.",
"Done": "Hotovo", "Done": "Hotovo",
"Drop the files to attach them": "Pustite súbory a priložia sa", "Drop the files to attach them": "Pustite súbory a priložia sa",
"Due %(when)s": "Naplánované na %(when)s", "Due %(when)s": "Naplánované na %(when)s",
@@ -2482,12 +2644,14 @@ const LEMBAS_I18N = {
"Not measured.": "Nezmerané.", "Not measured.": "Nezmerané.",
"Not run yet": "Ešte nebežala", "Not run yet": "Ešte nebežala",
"Not yet": "Ešte nie", "Not yet": "Ešte nie",
"Nothing has been run in this shell yet.": "V tomto shelli sa zatiaľ nič nespustilo.",
"Nothing has been said in the room yet.": "V miestnosti zatiaľ nikto nič nepovedal.", "Nothing has been said in the room yet.": "V miestnosti zatiaľ nikto nič nepovedal.",
"Nothing is scheduled.": "Nič nie je naplánované.", "Nothing is scheduled.": "Nič nie je naplánované.",
"Nothing matches.": "Nič nezodpovedá.", "Nothing matches.": "Nič nezodpovedá.",
"Nothing running.": "Nič nebeží.", "Nothing running.": "Nič nebeží.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Na pozadí nič nebeží. Podagenti, členovia tímu a príkazy na pozadí sa tu zobrazia, kým pracujú.", "Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Na pozadí nič nebeží. Podagenti, členovia tímu a príkazy na pozadí sa tu zobrazia, kým pracujú.",
"Nothing to report": "Nič na hlásenie", "Nothing to report": "Nič na hlásenie",
"Nothing to send: select some output first.": "Nie je čo odoslať: najprv označte časť výstupu.",
"Nothing yet.": "Zatiaľ nič.", "Nothing yet.": "Zatiaľ nič.",
"Notifications": "Upozornenia", "Notifications": "Upozornenia",
"One chat that grows": "Jedna konverzácia, ktorá rastie", "One chat that grows": "Jedna konverzácia, ktorá rastie",
@@ -2509,9 +2673,11 @@ const LEMBAS_I18N = {
"Plan — changes nothing": "Plan — nič nemení", "Plan — changes nothing": "Plan — nič nemení",
"Preview": "Náhľad", "Preview": "Náhľad",
"Project folders": "Priečinky projektov", "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",
"Read again": "Načítať znova", "Read again": "Načítať znova",
"Reasoning effort": "Náročnosť uvažovania", "Reasoning effort": "Náročnosť uvažovania",
"Recommended": "Odporúčané", "Recommended": "Odporúčané",
"Reconnecting…": "Znova sa pripája…",
"Regenerate reply": "Vygenerovať odpoveď znova", "Regenerate reply": "Vygenerovať odpoveď znova",
"Remove": "Odstrániť", "Remove": "Odstrániť",
"Remove %(dir)s": "Odstrániť %(dir)s", "Remove %(dir)s": "Odstrániť %(dir)s",
@@ -2537,6 +2703,7 @@ const LEMBAS_I18N = {
"Search": "Hľadať", "Search": "Hľadať",
"Search chats": "Hľadať v konverzáciách", "Search chats": "Hľadať v konverzáciách",
"Send": "Poslať", "Send": "Poslať",
"Send to chat": "Poslať do konverzácie",
"Sending…": "Odosiela sa…", "Sending…": "Odosiela sa…",
"Server": "Server", "Server": "Server",
"Set the time yourself": "Nastaviť čas sami", "Set the time yourself": "Nastaviť čas sami",
@@ -2545,6 +2712,7 @@ const LEMBAS_I18N = {
"Shown by the agent": "Ukázal agent", "Shown by the agent": "Ukázal agent",
"Sidebar": "Bočný panel", "Sidebar": "Bočný panel",
"Sign in": "Prihlásiť sa", "Sign in": "Prihlásiť sa",
"Sign in again: the server no longer accepts this app's sign-in.": "Prihláste sa znova: server už neprijíma prihlásenie tejto aplikácie.",
"Sign out": "Odhlásiť sa", "Sign out": "Odhlásiť sa",
"Signed in as %(who)s.": "Prihlásený ako %(who)s.", "Signed in as %(who)s.": "Prihlásený ako %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Odhlásením sa prihlásenie aplikácie na serveri zruší; prepojenie agenta zostane. Ak chcete použiť iný server, odhláste sa.", "Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Odhlásením sa prihlásenie aplikácie na serveri zruší; prepojenie agenta zostane. Ak chcete použiť iný server, odhláste sa.",
@@ -2567,6 +2735,7 @@ const LEMBAS_I18N = {
"Team: %(size)s of at most %(max)s subagents": "Tím: %(size)s z najviac %(max)s podagentov", "Team: %(size)s of at most %(max)s subagents": "Tím: %(size)s z najviac %(max)s podagentov",
"Tell me": "Upozorniť ma", "Tell me": "Upozorniť ma",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Upozorniť ma, keď je odpoveď alebo práca agenta hotová, kým som inde", "Tell me when a reply or an agent's work is finished while I am elsewhere": "Upozorniť ma, keď je odpoveď alebo práca agenta hotová, kým som inde",
"Terminal": "Terminál",
"The agent asks to run %(tool)s:": "Agent žiada spustiť %(tool)s:", "The agent asks to run %(tool)s:": "Agent žiada spustiť %(tool)s:",
"The agent asks:": "Agent sa pýta:", "The agent asks:": "Agent sa pýta:",
"The agent has finished.": "Agent dokončil prácu.", "The agent has finished.": "Agent dokončil prácu.",
@@ -2586,11 +2755,17 @@ const LEMBAS_I18N = {
"The last %(n)s runs failed: %(why)s": "Posledné spustenia zlyhali (%(n)s): %(why)s", "The last %(n)s runs failed: %(why)s": "Posledné spustenia zlyhali (%(n)s): %(why)s",
"The model's own": "Podľa modelu", "The model's own": "Podľa modelu",
"The most an agent chat may do here": "Najviac, čo tu agentská konverzácia smie", "The most an agent chat may do here": "Najviac, čo tu agentská konverzácia smie",
"The next command": "Nasledujúci príkaz",
"The next command (Ctrl+Shift+↓)": "Nasledujúci príkaz (Ctrl+Shift+↓)",
"The next runs": "Ďalšie spustenia", "The next runs": "Ďalšie spustenia",
"The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Čísla počítajú aj vlastného agenta konverzácie: 1 je agent sám, bez podagentov; Max 4 je agent a až 3 podagenti. Podagent beží na modeli konverzácie, na inom len vtedy, keď ho v konverzácii pomenujete.", "The numbers count this chat's own agent: 1 is the agent alone, with no subagents; Max 4 is the agent and up to 3 subagents. A subagent runs on the chat's model, or on another only when you name it in the chat.": "Čísla počítajú aj vlastného agenta konverzácie: 1 je agent sám, bez podagentov; Max 4 je agent a až 3 podagenti. Podagent beží na modeli konverzácie, na inom len vtedy, keď ho v konverzácii pomenujete.",
"The previous command": "Predchádzajúci príkaz",
"The previous command (Ctrl+Shift+↑)": "Predchádzajúci príkaz (Ctrl+Shift+↑)",
"The reply is ready.": "Odpoveď je hotová.", "The reply is ready.": "Odpoveď je hotová.",
"The result is no longer there.": "Výsledok už tu nie je.", "The result is no longer there.": "Výsledok už tu nie je.",
"The server refused this computer's link: %(why)s": "Server odmietol prepojenie tohto počítača: %(why)s", "The server refused this computer's link: %(why)s": "Server odmietol prepojenie tohto počítača: %(why)s",
"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.", "These words give no run in the future.": "Z týchto slov nevychádza žiadne budúce spustenie.",
"Think before answering": "Premýšľať pred odpoveďou", "Think before answering": "Premýšľať pred odpoveďou",
"Thinking": "Premýšľa", "Thinking": "Premýšľa",
@@ -2603,6 +2778,7 @@ const LEMBAS_I18N = {
"This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Tento model vybavuje naraz jednu požiadavku: podagenti potrebujú iný model na inom serveri, pomenovaný v konverzácii.", "This model serves one request at a time: subagents need another model, on another server, named in the chat.": "Tento model vybavuje naraz jednu požiadavku: podagenti potrebujú iný model na inom serveri, pomenovaný v konverzácii.",
"This server cannot compact a chat from the app yet; the web interface can.": "Tento server zatiaľ nevie zhrnúť konverzáciu z aplikácie; webové rozhranie to vie.", "This server cannot compact a chat from the app yet; the web interface can.": "Tento server zatiaľ nevie zhrnúť konverzáciu z aplikácie; webové rozhranie to vie.",
"This server has no scheduled tasks yet.": "Tento server zatiaľ nemá naplánované úlohy.", "This server has no scheduled tasks yet.": "Tento server zatiaľ nemá naplánované úlohy.",
"This terminal closed.": "Tento terminál sa zatvoril.",
"Thought": "Premýšľal", "Thought": "Premýšľal",
"Time": "Čas", "Time": "Čas",
"Times in %(zone)s": "Časové pásmo: %(zone)s", "Times in %(zone)s": "Časové pásmo: %(zone)s",
+20
View File
@@ -135,6 +135,26 @@
</div> </div>
</section> </section>
<aside class="termpanel" id="terminal" hidden aria-labelledby="terminal-title">
<header class="viewer__head termpanel__head">
<span class="termpanel__icon" id="terminal-icon" aria-hidden="true"></span>
<div class="viewer__id">
<h2 class="viewer__name" id="terminal-title" data-i18n>Terminal</h2>
<p class="viewer__where termpanel__where" id="terminal-where"></p>
</div>
<button type="button" class="tool" id="terminal-prev" hidden aria-label="The previous command" title="The previous command (Ctrl+Shift+↑)" data-i18n-aria-label data-i18n-title></button>
<button type="button" class="tool" id="terminal-next" hidden aria-label="The next command" title="The next command (Ctrl+Shift+↓)" data-i18n-aria-label data-i18n-title></button>
<button type="button" class="tool" id="terminal-copy" aria-label="Copy the last command and its output" title="Copy the last command and its output" data-i18n-aria-label data-i18n-title></button>
<button type="button" class="tool" id="terminal-close" aria-label="Close terminal" title="Close terminal" data-i18n-aria-label data-i18n-title></button>
</header>
<div class="termpanel__screen" id="terminal-screen"></div>
<footer class="termpanel__foot">
<p class="termpanel__message" id="terminal-message" aria-live="polite"></p>
<span class="termpanel__last" id="terminal-last"></span>
<button type="button" class="btn btn--sm termpanel__send" id="terminal-send" title="Put the last command and its output into the message box" data-i18n-title><span data-i18n>Send to chat</span></button>
</footer>
</aside>
<aside class="activity" id="activity" hidden aria-labelledby="activity-title"> <aside class="activity" id="activity" hidden aria-labelledby="activity-title">
<header class="viewer__head activity__head"> <header class="viewer__head activity__head">
<button type="button" class="tool" id="activity-back" hidden aria-label="Back" title="Back" data-i18n-aria-label data-i18n-title></button> <button type="button" class="tool" id="activity-back" hidden aria-label="Back" title="Back" data-i18n-aria-label data-i18n-title></button>
+55
View File
@@ -66,6 +66,61 @@ export async function follow(path, { onFrame, onEnd }) {
} }
} }
// --- The terminal panel's socket -----------------------------------------------------------------
//
// Opened by Rust (`terminal_open`), which holds the token and the socket; the page gets the frames
// as `terminal` events -- `output` (the shell's bytes, base64), `control` (the server's JSON), a last
// `end` -- and sends keys, sizes and "close" through commands. As with a followed reply, a frame
// can come before `terminal_open` has answered with its id: those wait.
const terminals = new Map()
const earlyTerminal = new Map()
listen("terminal", (e) => {
const { id, kind, data } = e.payload
const f = terminals.get(id)
if (f) deliverTerminal(f, id, kind, data)
else {
if (!earlyTerminal.has(id)) earlyTerminal.set(id, [])
earlyTerminal.get(id).push([kind, data])
}
})
function deliverTerminal(f, id, kind, data) {
if (kind === "end") {
terminals.delete(id)
f.onEnd(data || {})
} else if (kind === "output") f.onOutput(data)
else if (kind === "control") f.onControl(data)
}
/**
* Open a shell: `target` is `{ chat }` or `{ device, directory }`; `onOutput(base64)`,
* `onControl(frame)`, and `onEnd({ code, reason, status, error })` once. The handle sends keys
* (`input`), a size (`resize`), and lets go (`close`; onEnd still comes).
*/
export async function openTerminal(target, cols, rows, { onOutput, onControl, onEnd }) {
const id = await invoke("terminal_open", {
chat: target.chat ?? null,
device: target.device ?? null,
directory: target.directory ?? null,
cols,
rows,
})
const f = { onOutput, onControl, onEnd }
terminals.set(id, f)
for (const [kind, data] of earlyTerminal.get(id) || []) {
if (!terminals.has(id)) break
deliverTerminal(f, id, kind, data)
}
earlyTerminal.delete(id)
return {
id,
input: (data) => invoke("terminal_input", { id, data }),
resize: (c, r) => invoke("terminal_resize", { id, cols: c, rows: r }),
close: () => invoke("terminal_close", { id }),
}
}
/** A web link, in the system's browser. */ /** A web link, in the system's browser. */
export function openLink(url) { export function openLink(url) {
return invoke("open_external", { url }).catch(() => {}) return invoke("open_external", { url }).catch(() => {})
+59 -11
View File
@@ -18,10 +18,11 @@ import { readyIds, sending, sent, discard } from "./attach.js"
import { watchActivity, activityChanged, planList, activityButton } from "./activity.js" import { watchActivity, activityChanged, planList, activityButton } from "./activity.js"
import { agentBlock, agentReady, agentState, onAgentChange } from "./agent.js" import { agentBlock, agentReady, agentState, onAgentChange } from "./agent.js"
import { folderName } from "./list.js" import { folderName } from "./list.js"
import { aimTerminal, terminalButton, terminalOpen, toggleTerminal } from "./terminal.js"
const MODES = ["manual", "edit", "auto", "plan"] const MODES = ["manual", "edit", "auto", "plan"]
let ctx = null // set by initChat: { status(), thisDevices(), models(), me(), changed(chat), opened(chat), gone(chat) } let ctx = null // set by initChat: { status(), thisDevices(), devices(), models(), me(), changed(chat), opened(chat), gone(chat) }
let open = null // { chat, live, more, first } let open = null // { chat, live, more, first }
export function openChatId() { export function openChatId() {
@@ -34,6 +35,44 @@ export function onThisComputer(chat) {
return !!chat?.device && ctx.thisDevices().includes(chat.device.token_id) return !!chat?.device && ctx.thisDevices().includes(chat.device.token_id)
} }
/** A linked device, as `GET /devices` lists it (with `terminal`: whether it opens one). */
function deviceOf(id) {
return (ctx.devices?.() || []).find((d) => d.id === id) || null
}
/** The device a new agent chat goes to: this computer's, preferring the one linked right now. */
function draftDevice() {
const mine = ctx.thisDevices()
return mine.find((id) => deviceOf(id)) || mine[0] || ""
}
/**
* What the Terminal panel is for, as the page stands: a new agent chat whose device and folder
* are chosen (the shell before the first message), an open agent chat on a device that opens
* terminals, or nothing.
*/
function terminalTarget() {
if (draft) {
if (draft.kind !== "agent" || !draft.dir) return null
const device = deviceOf(draftDevice())
if (!device?.terminal) return null
return { device: device.id, directory: draft.dir, name: device.name || t("This computer") }
}
const chat = open?.chat
if (chat?.kind !== "agent" || !chat.device) return null
const device = deviceOf(chat.device.token_id)
if (!device?.terminal) return null
return { chat: chat.id, device: device.id, directory: chat.device.directory, name: device.name || t("Terminal") }
}
/** Text into the message box, after what is there, for the person to read before sending. */
export function intoComposer(text) {
const box = $("composer-text")
box.value = box.value.trim() ? `${box.value.replace(/\s*$/, "")}\n\n${text}` : text
box.dispatchEvent(new Event("input"))
box.focus()
}
function nearBottom() { function nearBottom() {
const th = $("thread") const th = $("thread")
return th.scrollHeight - th.scrollTop - th.clientHeight < 80 return th.scrollHeight - th.scrollTop - th.clientHeight < 80
@@ -578,15 +617,9 @@ function renderHead(chat) {
h("span", { class: "place__where", text: here ? t("This computer") : t("Another device") }), h("span", { class: "place__where", text: here ? t("This computer") : t("Another device") }),
), ),
) )
if (chat.device_session_id) { // The Terminal panel where the chat's device opens one (`GET /devices` says so per device);
tools.append( // "Continue in a terminal" stays in the chat's menu.
h( if (deviceOf(chat.device.token_id)?.terminal) tools.append(terminalButton())
"button",
{ type: "button", class: "tool", "aria-label": t("Continue in a terminal"), title: t("Continue in a terminal"), onclick: () => continueInTerminal(open?.chat || chat) },
icon(ICONS.terminal),
),
)
}
} }
tools.append(activityButton()) tools.append(activityButton())
tools.append(h("button", { type: "button", class: "tool", "aria-label": t("More"), title: t("More"), onclick: (e) => chatMenu(e.currentTarget, open?.chat || chat) }, icon(ICONS.more))) tools.append(h("button", { type: "button", class: "tool", "aria-label": t("More"), title: t("More"), onclick: (e) => chatMenu(e.currentTarget, open?.chat || chat) }, icon(ICONS.more)))
@@ -751,6 +784,8 @@ async function renderChips() {
parts.push(folderChip(roots, draft.dir, (d) => ((draft.dir = d), renderChips()))) parts.push(folderChip(roots, draft.dir, (d) => ((draft.dir = d), renderChips())))
parts.push(modeChip(draft.mode, (m) => ((draft.mode = m), renderChips()))) parts.push(modeChip(draft.mode, (m) => ((draft.mode = m), renderChips())))
} }
const shell = terminalTarget()
aimTerminal(shell)
parts.push(modelChip(models, draft.model, (id) => { parts.push(modelChip(models, draft.model, (id) => {
draft.model = id draft.model = id
draft.effort = levelsOf(models.find((m) => m.id === id)).start draft.effort = levelsOf(models.find((m) => m.id === id)).start
@@ -758,10 +793,19 @@ async function renderChips() {
}, mayPick)) }, mayPick))
if (model) parts.push(effortChip(model, draft.effort, (l) => ((draft.effort = l), renderChips()))) if (model) parts.push(effortChip(model, draft.effort, (l) => ((draft.effort = l), renderChips())))
if (draft.kind === "chat" && teamPossible(model)) parts.push(agentsChip(draft.agents, (a) => ((draft.agents = a), renderChips()), { cap: subagentsCap(), own: namedOnly(model?.subagents) })) if (draft.kind === "chat" && teamPossible(model)) parts.push(agentsChip(draft.agents, (a) => ((draft.agents = a), renderChips()), { cap: subagentsCap(), own: namedOnly(model?.subagents) }))
// 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 })
b.dataset.terminalToggle = ""
b.setAttribute("aria-pressed", String(terminalOpen()))
b.setAttribute("aria-controls", "terminal")
parts.push(b)
}
box.replaceChildren(...parts.filter(Boolean)) box.replaceChildren(...parts.filter(Boolean))
return return
} }
const chat = open?.chat const chat = open?.chat
aimTerminal(terminalTarget())
if (!chat?.kind) return box.replaceChildren() if (!chat?.kind) return box.replaceChildren()
const save = async (body) => { const save = async (body) => {
try { try {
@@ -809,6 +853,10 @@ export function statusChanged() {
if (draft) { if (draft) {
renderChips() renderChips()
renderHello() renderHello()
} else if (open?.chat?.kind) {
// The devices were read again: whether this chat's opens a terminal may have changed.
renderHead(open.chat)
aimTerminal(terminalTarget())
} }
} }
@@ -1092,7 +1140,7 @@ async function createChat(text, attachments) {
const agents = draft.agents const agents = draft.agents
if (agents && (agents.min || agents.max)) body.agents = agents if (agents && (agents.min || agents.max)) body.agents = agents
if (draft.kind === "agent") { if (draft.kind === "agent") {
const device = ctx.thisDevices()[0] const device = draftDevice()
if (!device || !agentReady(ctx.status())) { if (!device || !agentReady(ctx.status())) {
renderNotice() renderNotice()
throw { detail: agentState(ctx.status()).sentence } throw { detail: agentState(ctx.status()).sentence }
+1
View File
@@ -76,6 +76,7 @@ export const ICONS = {
max: "M4 4h8v8H4zM5.5 5.5v5h5v-5z", max: "M4 4h8v8H4zM5.5 5.5v5h5v-5z",
restore: "M5.5 3h7.5v7.5h-2.5V13H3V5.5h2.5zM7 4.5v1h3.5V9h1V4.5zM4.5 7v4.5H9V7z", restore: "M5.5 3h7.5v7.5h-2.5V13H3V5.5h2.5zM7 4.5v1h3.5V9h1V4.5zM4.5 7v4.5H9V7z",
chevronDown: "M3.5 6L8 10.5 12.5 6 11.4 4.9 8 8.3 4.6 4.9z", chevronDown: "M3.5 6L8 10.5 12.5 6 11.4 4.9 8 8.3 4.6 4.9z",
chevronUp: "M3.5 10L8 5.5 12.5 10 11.4 11.1 8 7.7 4.6 11.1z",
more: "M2.5 6.75h2.5v2.5H2.5zM6.75 6.75h2.5v2.5h-2.5zM11 6.75h2.5v2.5H11z", more: "M2.5 6.75h2.5v2.5H2.5zM6.75 6.75h2.5v2.5h-2.5zM11 6.75h2.5v2.5H11z",
edit: "M10.75 1.75l3.5 3.5-8.5 8.5h-3.5v-3.5zM3.75 10.87v1.38h1.38l6.62-6.62-1.38-1.38z", edit: "M10.75 1.75l3.5 3.5-8.5 8.5h-3.5v-3.5zM3.75 10.87v1.38h1.38l6.62-6.62-1.38-1.38z",
copy: "M5 1.5h8.5V11H5zM6.5 3v6.5H12V3zM2.5 5H4v8.5h6.5V15h-8z", copy: "M5 1.5h8.5V11H5zM6.5 3v6.5H12V3zM2.5 5H4v8.5h6.5V15h-8z",
+19 -3
View File
@@ -12,7 +12,8 @@ import { renderSettings, initSettings, scrollToSection } from "./settings.js"
import { initViewer, onViewer, closeViewer } from "./viewer.js" import { initViewer, onViewer, closeViewer } from "./viewer.js"
import { initExplorer, showLocal, showFilesTab } from "./explorer.js" import { initExplorer, showLocal, showFilesTab } from "./explorer.js"
import { initList, refreshList, markCurrent } from "./list.js" import { initList, refreshList, markCurrent } from "./list.js"
import { initChat, openChat, newChat, closeChat, openChatId, chatMenu, statusChanged, draftKind, onThisComputer, modelsChanged } from "./chat.js" import { initChat, openChat, newChat, closeChat, openChatId, chatMenu, statusChanged, draftKind, onThisComputer, modelsChanged, intoComposer } from "./chat.js"
import { initTerminal, showTerminal, terminalOpen, terminalAvailable, toggleTerminal, terminalThemeChanged } from "./terminal.js"
import { initAttach, allowAttach } from "./attach.js" import { initAttach, allowAttach } from "./attach.js"
import { initPalette, openPalette } from "./palette.js" import { initPalette, openPalette } from "./palette.js"
import { initActivity, showPanel, togglePanel, panelOpen } from "./activity.js" import { initActivity, showPanel, togglePanel, panelOpen } from "./activity.js"
@@ -67,6 +68,7 @@ function applyTheme(name) {
const root = document.documentElement const root = document.documentElement
root.dataset.theme = name root.dataset.theme = name
root.dataset.base = name root.dataset.base = name
terminalThemeChanged()
} }
// --- Views ------------------------------------------------------------------------------------ // --- Views ------------------------------------------------------------------------------------
@@ -162,7 +164,10 @@ function showPage(name) {
$("page-scheduled").hidden = name !== "scheduled" $("page-scheduled").hidden = name !== "scheduled"
$("settings").hidden = name !== "settings" $("settings").hidden = name !== "settings"
for (const b of document.querySelectorAll(".navrow[data-view]")) b.setAttribute("aria-current", String(b.dataset.view === name)) for (const b of document.querySelectorAll(".navrow[data-view]")) b.setAttribute("aria-current", String(b.dataset.view === name))
if (name !== "chat") showPanel(false) if (name !== "chat") {
showPanel(false)
showTerminal(false)
}
if (narrow()) sideOpen(false) if (narrow()) sideOpen(false)
} }
@@ -221,6 +226,7 @@ function commands() {
{ label: t("Settings"), run: () => showSettings(true) }, { label: t("Settings"), run: () => showSettings(true) },
] ]
if (openChatId()) out.push({ label: panelOpen() ? t("Close the activity panel") : t("Open the activity panel"), run: togglePanel }) if (openChatId()) out.push({ label: panelOpen() ? t("Close the activity panel") : t("Open the activity panel"), run: togglePanel })
if (terminalAvailable()) out.push({ label: terminalOpen() ? t("Close terminal") : t("Terminal"), run: toggleTerminal })
if (status?.linked) out.push({ label: status.agent_wanted ? t("Stop the agent") : t("Start the agent"), run: () => invoke("set_agent", { on: !status.agent_wanted }) }) if (status?.linked) out.push({ label: status.agent_wanted ? t("Stop the agent") : t("Start the agent"), run: () => invoke("set_agent", { on: !status.agent_wanted }) })
out.push( out.push(
{ label: status?.linked ? t("Link again") : t("Link this computer"), run: link }, { label: status?.linked ? t("Link again") : t("Link this computer"), run: link },
@@ -265,6 +271,7 @@ async function boot() {
initChat({ initChat({
status: () => status, status: () => status,
thisDevices, thisDevices,
devices: () => devices,
models: () => models, models: () => models,
me: () => me, me: () => me,
changed: () => refreshList(), changed: () => refreshList(),
@@ -289,9 +296,18 @@ async function boot() {
onViewer((on) => { onViewer((on) => {
$("shell").dataset.viewer = String(on) $("shell").dataset.viewer = String(on)
}) })
// One panel column: the activity panel and the terminal take turns in it (the shell stays open).
initTerminal({
onLayout: (on) => {
$("shell").dataset.terminal = String(on)
if (on && panelOpen()) showPanel(false)
},
composer: intoComposer,
})
initActivity({ initActivity({
onLayout: (on) => { onLayout: (on) => {
$("shell").dataset.activity = String(on) $("shell").dataset.activity = String(on)
if (on && terminalOpen()) showTerminal(false)
}, },
here: onThisComputer, here: onThisComputer,
// The conversation as it now stands: the summary's divider, read again, if it is still open. // The conversation as it now stands: the summary's divider, read again, if it is still open.
@@ -353,7 +369,7 @@ async function boot() {
renderBar(status) renderBar(status)
route() route()
// The agent came up or went: which devices are linked changes with it. // The agent came up or went: which devices are linked changes with it.
if (started && before && before.agent.state !== status.agent.state) loadDevices().then(refreshList) if (started && before && before.agent.state !== status.agent.state) loadDevices().then(() => (refreshList(), statusChanged()))
}) })
listen("open-settings", () => { listen("open-settings", () => {
if (view === "app") showSettings(true) if (view === "app") showSettings(true)
+491
View File
@@ -0,0 +1,491 @@
// The terminal panel: a shell on the device an agent chat works on, beside the chat -- the web
// page's panel (the server's static/js/terminal.js), in this app. Two places it opens:
//
// - an open agent chat: the chat's own shell (`{ chat }`), the one the web page shares;
// - a new agent chat, as soon as its device and folder are chosen (`{ device, directory }`):
// a shell there before the first message, which the chat that message makes takes over.
//
// The socket is Rust's (bridge.js `openTerminal`): the page never holds the token. xterm is the
// server's vendored build (ui/vendor, scripts/fetch-vendor.sh), loaded the first time the panel
// opens. Never beside the activity panel: the shell's layout has one panel column (as the web
// page has), so opening one closes the other -- the shell stays open behind it.
//
// Three things about xterm the web panel learnt the hard way, kept here: it cannot fit while its
// box is hidden (so every fit waits until the panel is shown), the panel changes size without a
// window `resize` (so a ResizeObserver), and it reads no CSS variables (so the theme is read from
// the computed tokens, and again when the theme changes).
import { $, h, icon, ICONS } from "./dom.js"
import { openTerminal, why } from "./bridge.js"
const ANSI = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "brightBlack", "brightRed", "brightGreen", "brightYellow", "brightBlue", "brightMagenta", "brightCyan", "brightWhite"]
const MAX_MARKED = 200
let ctx = { layout: () => {}, composer: () => {} }
let target = null // what the panel is for: { key, chat } | { key, device, directory }, with `name`
let term = null
let fit = null
let session = null // the open socket's handle, and which target it is for: { handle, key, draft }
let observer = null
let loading = null
// A device's shell marks its own commands (OSC 133 A/B/C/D, OSC 7 for where it is) and says so
// in its ready frame (`osc`): then a command and its output are read from the panel's own
// buffer. Without the marks, "send" falls back to the screen as it is, and says so.
let osc = false
let pending = null
let finished = []
let visiting = -1
const panel = () => $("terminal")
function say(text, isError) {
const el = $("terminal-message")
el.textContent = text || ""
el.classList.toggle("is-error", !!isError)
}
/** Whether the panel is shown. */
export function terminalOpen() {
return !panel().hidden
}
/** Whether there is a shell to open for what the chat page shows now. */
export function terminalAvailable() {
return !!target
}
// --- xterm, loaded once --------------------------------------------------------------------------
function load() {
if (loading) return loading
loading = new Promise((resolve, reject) => {
if (typeof window.Terminal === "function" && window.FitAddon) return resolve()
document.head.append(h("link", { rel: "stylesheet", href: "vendor/xterm.css" }))
const script = (src) =>
new Promise((ok, fail) => {
const s = h("script", { src })
s.addEventListener("load", ok)
s.addEventListener("error", () => fail(new Error(src)))
document.head.append(s)
})
script("vendor/xterm.js")
.then(() => script("vendor/xterm-addon-fit.js"))
.then(resolve, reject)
})
loading.catch(() => {
loading = null
})
return loading
}
function readTheme() {
const style = getComputedStyle(document.documentElement)
const token = (name, fallback) => (style.getPropertyValue(name) || "").trim() || fallback
const theme = {
background: token("--code-bg", "black"),
foreground: token("--ink", "white"),
cursor: token("--accent", "white"),
cursorAccent: token("--code-bg", "black"),
selectionBackground: token("--accent-soft", "gray"),
}
for (const name of ANSI) {
const value = token(`--ansi-${name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`, "")
if (value) theme[name] = value
}
return theme
}
/** The theme changed: xterm's colours are read again. */
export function terminalThemeChanged() {
if (term) term.options.theme = readTheme()
}
function build() {
if (term) return
const style = getComputedStyle(document.documentElement)
term = new window.Terminal({
allowProposedApi: true,
cursorBlink: true,
fontFamily: style.getPropertyValue("--font-mono").trim() || "monospace",
fontSize: parseFloat(style.getPropertyValue("--terminal-font-size")) || 13,
scrollback: 5000,
theme: readTheme(),
})
fit = new window.FitAddon.FitAddon()
term.loadAddon(fit)
term.open($("terminal-screen"))
term.parser.registerOscHandler(133, onCommandMark)
term.parser.registerOscHandler(7, onWhere)
term.onData((data) => session?.handle?.input(data))
// Ctrl+C is the shell's interrupt; copy and paste are the shifted pair, as in every terminal.
term.attachCustomKeyEventHandler((e) => {
if (!e.ctrlKey || !e.shiftKey || e.type !== "keydown") return true
if (osc && (e.key === "ArrowUp" || e.key === "ArrowDown")) {
visit(e.key === "ArrowUp" ? -1 : 1)
return false
}
const key = e.key.toLowerCase()
if (key === "c") {
const text = term.getSelection()
if (text) navigator.clipboard.writeText(text).catch(() => {})
return false
}
if (key === "v") {
navigator.clipboard
.readText()
.then((text) => session?.handle?.input(text))
.catch(() => {})
return false
}
return true
})
observer = new ResizeObserver(() => refit())
observer.observe($("terminal-screen"))
}
function refit() {
if (!term || !fit || !terminalOpen() || !$("terminal-screen").offsetWidth) return
try {
fit.fit()
} catch {
return
}
session?.handle?.resize(term.cols, term.rows)
}
// --- The socket ------------------------------------------------------------------------------------
function decode(b64) {
const raw = atob(b64)
const bytes = new Uint8Array(raw.length)
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i)
return bytes
}
async function connect() {
if (!target || !term || session) return
const t0 = target
// The session exists before the socket answers, so frames that come first are this one's.
const mine = { handle: null, key: t0.key, draft: !t0.chat }
session = mine
say(t("Connecting…"))
let handle
try {
handle = await openTerminal(t0.chat ? { chat: t0.chat } : { device: t0.device, directory: t0.directory }, term.cols || 80, term.rows || 24, {
onOutput: (b64) => {
if (session === mine) term.write(decode(b64))
},
onControl: (frame) => {
if (session === mine) control(frame)
},
onEnd: (end) => {
if (session !== mine) return
session = null
ended(end)
},
})
} catch (err) {
if (session === mine) session = null
say(why(err), true)
return
}
mine.handle = handle
// Let go of meanwhile (another chat, the panel's target changed): closed at once.
if (session !== mine) handle.close()
}
function control(frame) {
if (frame.t === "ready") {
say(frame.shared ? t("Connected. This shell is also open in another tab, and they share a size.") : t("Connected."))
if (frame.dir) $("terminal-where").textContent = frame.dir
osc = !!frame.osc
showMarkControls()
showLast("")
refit()
if (terminalOpen()) term.focus()
} else if (frame.t === "behind") {
// This window stopped reading and was let go so the others kept up. The scrollback is the
// state: connecting again costs nothing.
say(frame.message || t("Reconnecting…"))
drop()
term.reset()
forgetMarks()
connect()
} else if (frame.t === "closed" || frame.t === "error") {
// No reconnect of its own: a new shell has lost the folder, the environment and the
// half-typed command, and quietly putting one in its place is worse than saying it went.
say(frame.message || t("This terminal closed."), frame.t === "error")
drop()
}
}
function ended(end) {
if (end.code === 4401 || end.status === 401) say(t("Sign in again: the server no longer accepts this app's sign-in."), true)
else if (end.status === 404) say(t("The terminal needs a newer LLeMbas server. Ask its administrator to update it."), true)
else if (end.error) say(end.error, true)
else say(t("Disconnected. Close and reopen to reconnect."))
}
/** Let go of the socket (the shell on the device lives on, as the server keeps it). */
function drop() {
const s = session
session = null
s?.handle?.close()
}
// --- What the panel is for -----------------------------------------------------------------------
/**
* Tell the panel what the chat page shows: `{ chat, name, directory }` for an open agent chat,
* `{ device, directory, name }` for a new one with its device and folder chosen, or null (no shell
* to open). A new chat's shell becomes its chat's when the first message has made the chat: the
* server took that shell over, so the socket stays as it is.
*/
export function aimTerminal(next) {
const key = next ? (next.chat ? `chat:${next.chat}` : `device:${next.device}:${next.directory}`) : ""
const before = target
target = next ? { ...next, key } : null
if (target) {
$("terminal-title").textContent = target.name || t("Terminal")
$("terminal-where").textContent = target.directory || ""
$("terminal-where").title = target.directory || ""
}
if (before?.key === key) return
// The draft's shell is the new chat's now: the same socket, said for the chat from here on.
const adopted = session?.draft && before && !before.chat && target?.chat && before.directory === target.directory && before.device === target.device
if (adopted) {
session.key = key
session.draft = false
return
}
if (session) {
drop()
if (term) term.reset()
forgetMarks()
say("")
}
if (!target) {
if (terminalOpen()) showTerminal(false)
return
}
if (terminalOpen()) connect()
}
// --- Showing it ------------------------------------------------------------------------------------
/** Show or hide the panel. Hiding keeps the shell and its socket: reopening is instant. */
export async function showTerminal(on) {
if (on && !target) return
panel().hidden = !on
ctx.layout(on)
markOpen()
if (!on) return
try {
await load()
} catch {
say(t("The terminal could not be loaded."), true)
return
}
if (!terminalOpen()) return
build()
// A frame later: the panel has only just been shown and has no width to fit to yet.
requestAnimationFrame(() => {
refit()
connect()
term.focus()
})
}
export function toggleTerminal() {
showTerminal(!terminalOpen())
}
function markOpen() {
for (const b of document.querySelectorAll("[data-terminal-toggle]")) b.setAttribute("aria-pressed", String(terminalOpen()))
}
/** The header's button for an open chat; a new chat has the chip under the composer instead. */
export function terminalButton() {
return h(
"button",
{
type: "button",
class: "tool",
"aria-label": t("Terminal"),
title: t("Terminal"),
"aria-controls": "terminal",
"aria-pressed": String(terminalOpen()),
"data-terminal-toggle": "",
onclick: toggleTerminal,
},
icon(ICONS.terminal),
)
}
// --- A device shell's own marks ----------------------------------------------------------------------
function onCommandMark(data) {
if (!osc || !term) return false
const [kind, code] = String(data).split(";")
if (kind === "A") pending = { prompt: term.registerMarker(0) }
else if (kind === "B") {
pending = pending || {}
pending.input = term.registerMarker(0)
pending.inputX = term.buffer.active.cursorX
} else if (kind === "C") {
pending = pending || {}
pending.output = term.registerMarker(0)
pending.command = commandText(pending)
} else if (kind === "D") {
if (pending?.output) {
pending.exit = parseInt(code, 10) || 0
pending.end = term.registerMarker(0)
commandDone(pending)
}
pending = null
}
return true
}
function onWhere(data) {
if (!osc) return false
let path = String(data).replace(/^file:\/\/[^/]*/, "")
try {
path = decodeURIComponent(path)
} catch {}
if (path) $("terminal-where").textContent = path
return true
}
function linesBetween(from, to, firstX) {
const buffer = term.buffer.active
const out = []
for (let y = from; y < to; y++) {
const line = buffer.getLine(y)
if (!line) continue
const text = line.translateToString(true, y === from ? firstX || 0 : 0)
if (line.isWrapped && out.length) out[out.length - 1] += text
else out.push(text)
}
while (out.length && !out[out.length - 1].trim()) out.pop()
return out.join("\n")
}
function commandText(mark) {
if (!mark.input || !mark.output || mark.input.line < 0) return ""
return linesBetween(mark.input.line, Math.max(mark.output.line, mark.input.line + 1), mark.inputX).trim()
}
function outputOf(mark) {
if (!mark.output || !mark.end || mark.output.line < 0 || mark.end.line < 0) return ""
return linesBetween(mark.output.line, mark.end.line, 0)
}
function commandDone(mark) {
finished.push(mark)
if (finished.length > MAX_MARKED) finished.shift()
visiting = -1
showLast((mark.command || "…") + (mark.exit ? ` · exit ${mark.exit}` : ""))
}
function blockOf(mark) {
const where = $("terminal-where").textContent
const head = `${where ? `${where} ` : ""}$ ${mark.command || ""}`
return "```\n" + head + "\n" + outputOf(mark) + "\n```\n" + (mark.exit ? `exit ${mark.exit}\n` : "")
}
function lastFinished() {
for (let i = finished.length - 1; i >= 0; i--) if (finished[i].output && finished[i].output.line >= 0) return finished[i]
return null
}
function visit(step) {
const live = finished.filter((m) => (m.prompt || m.input || m.output)?.line >= 0)
if (!live.length) return
visiting = visiting < 0 ? live.length : visiting
visiting = Math.max(0, Math.min(live.length - 1, visiting + step))
const m = live[visiting]
term.scrollToLine((m.prompt || m.input || m.output).line)
}
function forgetMarks() {
finished = []
pending = null
visiting = -1
osc = false
showMarkControls()
showLast("")
}
function showMarkControls() {
$("terminal-prev").hidden = !osc
$("terminal-next").hidden = !osc
}
function showLast(text) {
$("terminal-last").textContent = text || ""
}
// --- Handing it to the chat --------------------------------------------------------------------------
/** What "send" and "copy" take: the selection; else the last command (with marks); else the screen. */
function capture() {
if (!term) return ""
const chosen = term.getSelection()
if (chosen && chosen.trim()) return "```\n" + chosen + "\n```\n"
if (osc) {
const mark = lastFinished()
if (!mark) {
say(t("Nothing has been run in this shell yet."))
return ""
}
return blockOf(mark)
}
const buffer = term.buffer.active
const last = buffer.baseY + buffer.cursorY
const lines = []
for (let y = Math.max(0, last - 40); y <= last; y++) {
const line = buffer.getLine(y)
if (line) lines.push(line.translateToString(true))
}
const text = lines.join("\n").replace(/\n+$/, "")
if (!text.trim()) {
say(t("Nothing to send: select some output first."))
return ""
}
say(t("Copied the last of the screen, as it appeared. This shell does not mark where commands begin."))
return "```\n" + text + "\n```\n"
}
/** Into the message box, never sent: what a machine printed is for the person to read first. */
function sendToChat() {
const text = capture()
if (!text) return
ctx.composer(text)
// Where the panel covers the chat (a narrow window), it steps aside to show the message box.
if (window.matchMedia("(max-width: 64rem)").matches) showTerminal(false)
}
function copyOut() {
const text = capture()
if (!text) return
navigator.clipboard.writeText(text).then(
() => say(t("Copied.")),
() => {},
)
}
export function initTerminal({ onLayout, composer }) {
ctx = { layout: onLayout || (() => {}), composer: composer || (() => {}) }
$("terminal-icon").replaceChildren(icon(ICONS.terminal))
$("terminal-prev").replaceChildren(icon(ICONS.chevronUp))
$("terminal-next").replaceChildren(icon(ICONS.chevronDown))
$("terminal-copy").replaceChildren(icon(ICONS.copy))
$("terminal-send").prepend(icon(ICONS.send))
$("terminal-close").replaceChildren(icon(ICONS.close))
$("terminal-prev").addEventListener("click", () => visit(-1))
$("terminal-next").addEventListener("click", () => visit(1))
$("terminal-copy").addEventListener("click", copyOut)
$("terminal-send").addEventListener("click", sendToChat)
$("terminal-close").addEventListener("click", () => showTerminal(false))
}
+2
View File
@@ -0,0 +1,2 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
//# sourceMappingURL=addon-fit.js.map
+218
View File
@@ -0,0 +1,218 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js
* @license MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/
/**
* Default styles for xterm.js
*/
.xterm {
cursor: text;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 5;
}
.xterm .xterm-helper-textarea {
padding: 0;
border: 0;
margin: 0;
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -5;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: #000;
color: #FFF;
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm .xterm-scroll-area {
visibility: hidden;
}
.xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0;
left: -9999em;
line-height: normal;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.xterm.xterm-cursor-pointer,
.xterm .xterm-cursor-pointer {
cursor: pointer;
}
.xterm.column-select.focus {
/* Column selection mode */
cursor: crosshair;
}
.xterm .xterm-accessibility:not(.debug),
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
color: transparent;
pointer-events: none;
}
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
color: transparent;
}
.xterm .xterm-accessibility-tree {
user-select: text;
white-space: pre;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.xterm-dim {
/* Dim should not apply to background, so the opacity of the foreground color is applied
* explicitly in the generated class and reset to 1 here */
opacity: 1 !important;
}
.xterm-underline-1 { text-decoration: underline; }
.xterm-underline-2 { text-decoration: double underline; }
.xterm-underline-3 { text-decoration: wavy underline; }
.xterm-underline-4 { text-decoration: dotted underline; }
.xterm-underline-5 { text-decoration: dashed underline; }
.xterm-overline {
text-decoration: overline;
}
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
.xterm-strikethrough {
text-decoration: line-through;
}
.xterm-screen .xterm-decoration-container .xterm-decoration {
z-index: 6;
position: absolute;
}
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
z-index: 7;
}
.xterm-decoration-overview-ruler {
z-index: 8;
position: absolute;
top: 0;
right: 0;
pointer-events: none;
}
.xterm-decoration-top {
z-index: 2;
position: relative;
}
+2
View File
@@ -0,0 +1,2 @@
!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var i=t();for(var s in i)("object"==typeof exports?exports:e)[s]=i[s]}}(globalThis,(()=>(()=>{"use strict";var e={4567:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.AccessibilityManager=void 0;const n=i(9042),o=i(9924),a=i(844),h=i(4725),c=i(2585),l=i(3656);let d=t.AccessibilityManager=class extends a.Disposable{constructor(e,t,i,s){super(),this._terminal=e,this._coreBrowserService=i,this._renderService=s,this._rowColumns=new WeakMap,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce="",this._accessibilityContainer=this._coreBrowserService.mainDocument.createElement("div"),this._accessibilityContainer.classList.add("xterm-accessibility"),this._rowContainer=this._coreBrowserService.mainDocument.createElement("div"),this._rowContainer.setAttribute("role","list"),this._rowContainer.classList.add("xterm-accessibility-tree"),this._rowElements=[];for(let e=0;e<this._terminal.rows;e++)this._rowElements[e]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[e]);if(this._topBoundaryFocusListener=e=>this._handleBoundaryFocus(e,0),this._bottomBoundaryFocusListener=e=>this._handleBoundaryFocus(e,1),this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions(),this._accessibilityContainer.appendChild(this._rowContainer),this._liveRegion=this._coreBrowserService.mainDocument.createElement("div"),this._liveRegion.classList.add("live-region"),this._liveRegion.setAttribute("aria-live","assertive"),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this.register(new o.TimeBasedDebouncer(this._renderRows.bind(this))),!this._terminal.element)throw new Error("Cannot enable accessibility before Terminal.open");this._terminal.element.insertAdjacentElement("afterbegin",this._accessibilityContainer),this.register(this._terminal.onResize((e=>this._handleResize(e.rows)))),this.register(this._terminal.onRender((e=>this._refreshRows(e.start,e.end)))),this.register(this._terminal.onScroll((()=>this._refreshRows()))),this.register(this._terminal.onA11yChar((e=>this._handleChar(e)))),this.register(this._terminal.onLineFeed((()=>this._handleChar("\n")))),this.register(this._terminal.onA11yTab((e=>this._handleTab(e)))),this.register(this._terminal.onKey((e=>this._handleKey(e.key)))),this.register(this._terminal.onBlur((()=>this._clearLiveRegion()))),this.register(this._renderService.onDimensionsChange((()=>this._refreshRowsDimensions()))),this.register((0,l.addDisposableDomListener)(document,"selectionchange",(()=>this._handleSelectionChange()))),this.register(this._coreBrowserService.onDprChange((()=>this._refreshRowsDimensions()))),this._refreshRows(),this.register((0,a.toDisposable)((()=>{this._accessibilityContainer.remove(),this._rowElements.length=0})))}_handleTab(e){for(let t=0;t<e;t++)this._handleChar(" ")}_handleChar(e){this._liveRegionLineCount<21&&(this._charsToConsume.length>0?this._charsToConsume.shift()!==e&&(this._charsToAnnounce+=e):this._charsToAnnounce+=e,"\n"===e&&(this._liveRegionLineCount++,21===this._liveRegionLineCount&&(this._liveRegion.textContent+=n.tooMuchOutput)))}_clearLiveRegion(){this._liveRegion.textContent="",this._liveRegionLineCount=0}_handleKey(e){this._clearLiveRegion(),/\p{Control}/u.test(e)||this._charsToConsume.push(e)}_refreshRows(e,t){this._liveRegionDebouncer.refresh(e,t,this._terminal.rows)}_renderRows(e,t){const i=this._terminal.buffer,s=i.lines.length.toString();for(let r=e;r<=t;r++){const e=i.lines.get(i.ydisp+r),t=[],n=e?.translateToString(!0,void 0,void 0,t)||"",o=(i.ydisp+r+1).toString(),a=this._rowElements[r];a&&(0===n.length?(a.innerText=" ",this._rowColumns.set(a,[0,1])):(a.textContent=n,this._rowColumns.set(a,t)),a.setAttribute("aria-posinset",o),a.setAttribute("aria-setsize",s))}this._announceCharacters()}_announceCharacters(){0!==this._charsToAnnounce.length&&(this._liveRegion.textContent+=this._charsToAnnounce,this._charsToAnnounce="")}_handleBoundaryFocus(e,t){const i=e.target,s=this._rowElements[0===t?1:this._rowElements.length-2];if(i.getAttribute("aria-posinset")===(0===t?"1":`${this._terminal.buffer.lines.length}`))return;if(e.relatedTarget!==s)return;let r,n;if(0===t?(r=i,n=this._rowElements.pop(),this._rowContainer.removeChild(n)):(r=this._rowElements.shiftLine truncated
//# sourceMappingURL=xterm.js.map
+3
View File
@@ -19,6 +19,9 @@
"lezer-common.mjs": {"server": "b63d03ca460cc7a6ce09cbf0de24943fec22bcbe2359dc35978a57b875b2450f", "copy": "b63d03ca460cc7a6ce09cbf0de24943fec22bcbe2359dc35978a57b875b2450f"}, "lezer-common.mjs": {"server": "b63d03ca460cc7a6ce09cbf0de24943fec22bcbe2359dc35978a57b875b2450f", "copy": "b63d03ca460cc7a6ce09cbf0de24943fec22bcbe2359dc35978a57b875b2450f"},
"lezer-highlight.mjs": {"server": "b74148d673103ae08330a045a464acddf4ee8b563b213ea26e50f2be01a6d19d", "copy": "df21a1741ae80f2ad2b6d25860cad9ff3cda246a4fba171535d9dc92df150c1a"}, "lezer-highlight.mjs": {"server": "b74148d673103ae08330a045a464acddf4ee8b563b213ea26e50f2be01a6d19d", "copy": "df21a1741ae80f2ad2b6d25860cad9ff3cda246a4fba171535d9dc92df150c1a"},
"lezer-lr.mjs": {"server": "4c2dcc61b5c3aedb091dfd60ce8abaeb94704b58fa9036547b0b8d2c75f701e4", "copy": "f41bc21dc3fd874132e1d9862aae57946ae2604d91330303924c03aecfd3bc21"}, "lezer-lr.mjs": {"server": "4c2dcc61b5c3aedb091dfd60ce8abaeb94704b58fa9036547b0b8d2c75f701e4", "copy": "f41bc21dc3fd874132e1d9862aae57946ae2604d91330303924c03aecfd3bc21"},
"xterm.js": {"server": "1f991ac3b4b283ebf96e60ae23a00a52765dd3a2e46fa6fdda9f1aab032f7495", "copy": "1f991ac3b4b283ebf96e60ae23a00a52765dd3a2e46fa6fdda9f1aab032f7495"},
"xterm.css": {"server": "ba8e6985669488981ccf40c0cefe3aba80722cb6c92de7ad628b0bd717faf2b6", "copy": "ba8e6985669488981ccf40c0cefe3aba80722cb6c92de7ad628b0bd717faf2b6"},
"xterm-addon-fit.js": {"server": "bdaefa370b1bfc42ee88d46fe6072400902a4d4b2d45cd93438dda9b23c97089", "copy": "bdaefa370b1bfc42ee88d46fe6072400902a4d4b2d45cd93438dda9b23c97089"},
"node-process.mjs": {"server": "0c7460cbd81d8427a882867726b6db43beff3399f045ad942f4a65fef4705981", "copy": "0c7460cbd81d8427a882867726b6db43beff3399f045ad942f4a65fef4705981"} "node-process.mjs": {"server": "0c7460cbd81d8427a882867726b6db43beff3399f045ad942f4a65fef4705981", "copy": "0c7460cbd81d8427a882867726b6db43beff3399f045ad942f4a65fef4705981"}
} }
} }