Terminal: a shell on the agent chat's computer, before the first message too
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:
21 files changed
+1989
-24
No files matched your search
+58
-2
@@ -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-viewer="true"] { --panel-col: minmax(var(--inspector-width-min), 42%); }
|
||||
.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 --------------------------------------------------------------------------- */
|
||||
|
||||
@@ -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: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__label { overflow: hidden; text-overflow: ellipsis; }
|
||||
.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 .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 ---------------------------------------------------------------------- */
|
||||
|
||||
/* 64rem (--bp-wide): a panel goes over the chat instead of beside it. */
|
||||
@media (max-width: 64rem) {
|
||||
.shell[data-viewer="true"], .shell[data-activity="true"] { --panel-col: 0px; }
|
||||
.viewer, .activity, .shell[data-activity="true"] .viewer { grid-area: main; z-index: var(--z-panel); border-left: 0; }
|
||||
.shell[data-viewer="true"], .shell[data-activity="true"], .shell[data-terminal="true"] { --panel-col: 0px; }
|
||||
.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__next { display: none; }
|
||||
}
|
||||
|
||||
+176
@@ -68,6 +68,7 @@ const LEMBAS_I18N = {
|
||||
"Choose a model to choose its effort.": "Vyberte model, abyste mohli zvolit jeho náročnost.",
|
||||
"Close": "Zavřít",
|
||||
"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",
|
||||
"Command": "Příkaz",
|
||||
"Commands": "Příkazy",
|
||||
@@ -77,12 +78,18 @@ const LEMBAS_I18N = {
|
||||
"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.",
|
||||
"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 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…",
|
||||
"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 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é.",
|
||||
"Cut short here; the whole of it is in the web interface.": "Zde zkráceno; celé je to ve webovém rozhraní.",
|
||||
"Days": "Dny",
|
||||
@@ -91,6 +98,7 @@ const LEMBAS_I18N = {
|
||||
"Delete this chat?": "Smazat tuto konverzaci?",
|
||||
"Delete this scheduled task?": "Odstranit tuto naplánovanou úlohu?",
|
||||
"Deny": "Zamítnout",
|
||||
"Disconnected. Close and reopen to reconnect.": "Odpojeno. Pro opětovné připojení zavřete a znovu otevřete.",
|
||||
"Done": "Hotovo",
|
||||
"Drop the files to attach them": "Pusťte soubory a přiloží se",
|
||||
"Due %(when)s": "Naplánováno na %(when)s",
|
||||
@@ -165,12 +173,14 @@ const LEMBAS_I18N = {
|
||||
"Not measured.": "Neměřeno.",
|
||||
"Not run yet": "Ještě neběžela",
|
||||
"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 is scheduled.": "Nic není naplánováno.",
|
||||
"Nothing matches.": "Nic neodpovídá.",
|
||||
"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 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.",
|
||||
"Notifications": "Upozornění",
|
||||
"One chat that grows": "Jedna konverzace, která roste",
|
||||
@@ -192,9 +202,11 @@ const LEMBAS_I18N = {
|
||||
"Plan — changes nothing": "Plan — nic nemění",
|
||||
"Preview": "Náhled",
|
||||
"Project folders": "Složky projektů",
|
||||
"Put the last command and its output into the message box": "Vložit poslední příkaz a jeho výstup do pole zprávy",
|
||||
"Read again": "Načíst znovu",
|
||||
"Reasoning effort": "Náročnost uvažování",
|
||||
"Recommended": "Doporučeno",
|
||||
"Reconnecting…": "Znovu se připojuje…",
|
||||
"Regenerate reply": "Vygenerovat odpověď znovu",
|
||||
"Remove": "Odebrat",
|
||||
"Remove %(dir)s": "Odebrat %(dir)s",
|
||||
@@ -220,6 +232,7 @@ const LEMBAS_I18N = {
|
||||
"Search": "Hledat",
|
||||
"Search chats": "Hledat v konverzacích",
|
||||
"Send": "Odeslat",
|
||||
"Send to chat": "Odeslat do konverzace",
|
||||
"Sending…": "Odesílá se…",
|
||||
"Server": "Server",
|
||||
"Set the time yourself": "Nastavit čas sami",
|
||||
@@ -228,6 +241,7 @@ const LEMBAS_I18N = {
|
||||
"Shown by the agent": "Ukázal agent",
|
||||
"Sidebar": "Postranní panel",
|
||||
"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",
|
||||
"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.",
|
||||
@@ -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ů",
|
||||
"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",
|
||||
"Terminal": "Terminál",
|
||||
"The agent asks to run %(tool)s:": "Agent žádá o spuštění %(tool)s:",
|
||||
"The agent asks:": "Agent se ptá:",
|
||||
"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 model's own": "Podle modelu",
|
||||
"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 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 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 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í.",
|
||||
"Think before answering": "Přemýšlet před odpovědí",
|
||||
"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 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 terminal closed.": "Tento terminál se zavřel.",
|
||||
"Thought": "Přemýšlel",
|
||||
"Time": "Čas",
|
||||
"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.",
|
||||
"Close": "Schließen",
|
||||
"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",
|
||||
"Command": "Befehl",
|
||||
"Commands": "Befehle",
|
||||
@@ -408,12 +431,18 @@ const LEMBAS_I18N = {
|
||||
"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.",
|
||||
"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 and background tasks": "Kontext und Hintergrundaufgaben",
|
||||
"Continue in a terminal": "Im Terminal fortsetzen",
|
||||
"Continue in a terminal…": "Im Terminal fortsetzen…",
|
||||
"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 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.",
|
||||
"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",
|
||||
@@ -422,6 +451,7 @@ const LEMBAS_I18N = {
|
||||
"Delete this chat?": "Diesen Chat löschen?",
|
||||
"Delete this scheduled task?": "Diese geplante Aufgabe löschen?",
|
||||
"Deny": "Ablehnen",
|
||||
"Disconnected. Close and reopen to reconnect.": "Getrennt. Schließen und erneut öffnen, um neu zu verbinden.",
|
||||
"Done": "Fertig",
|
||||
"Drop the files to attach them": "Dateien ablegen, um sie anzuhängen",
|
||||
"Due %(when)s": "Fällig %(when)s",
|
||||
@@ -496,12 +526,14 @@ const LEMBAS_I18N = {
|
||||
"Not measured.": "Nicht gemessen.",
|
||||
"Not run yet": "Noch nicht gelaufen",
|
||||
"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 is scheduled.": "Nichts ist geplant.",
|
||||
"Nothing matches.": "Nichts gefunden.",
|
||||
"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 to report": "Nichts zu melden",
|
||||
"Nothing to send: select some output first.": "Nichts zu senden: Markieren Sie zuerst etwas Ausgabe.",
|
||||
"Nothing yet.": "Noch nichts.",
|
||||
"Notifications": "Benachrichtigungen",
|
||||
"One chat that grows": "Ein Chat, der wächst",
|
||||
@@ -523,9 +555,11 @@ const LEMBAS_I18N = {
|
||||
"Plan — changes nothing": "Plan — ändert nichts",
|
||||
"Preview": "Vorschau",
|
||||
"Project folders": "Projektordner",
|
||||
"Put the last command and its output into the message box": "Den letzten Befehl und seine Ausgabe ins Nachrichtenfeld legen",
|
||||
"Read again": "Neu lesen",
|
||||
"Reasoning effort": "Denkaufwand",
|
||||
"Recommended": "Empfohlen",
|
||||
"Reconnecting…": "Verbinde neu…",
|
||||
"Regenerate reply": "Antwort neu generieren",
|
||||
"Remove": "Entfernen",
|
||||
"Remove %(dir)s": "%(dir)s entfernen",
|
||||
@@ -551,6 +585,7 @@ const LEMBAS_I18N = {
|
||||
"Search": "Suchen",
|
||||
"Search chats": "Chats durchsuchen",
|
||||
"Send": "Senden",
|
||||
"Send to chat": "An den Chat senden",
|
||||
"Sending…": "Wird gesendet…",
|
||||
"Server": "Server",
|
||||
"Set the time yourself": "Zeit selbst festlegen",
|
||||
@@ -559,6 +594,7 @@ const LEMBAS_I18N = {
|
||||
"Shown by the agent": "Vom Agenten gezeigt",
|
||||
"Sidebar": "Seitenleiste",
|
||||
"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",
|
||||
"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.",
|
||||
@@ -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",
|
||||
"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",
|
||||
"Terminal": "Terminal",
|
||||
"The agent asks to run %(tool)s:": "Der Agent möchte %(tool)s ausführen:",
|
||||
"The agent asks:": "Der Agent fragt:",
|
||||
"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 model's own": "Die des Modells",
|
||||
"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 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 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 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.",
|
||||
"Think before answering": "Vor der Antwort nachdenken",
|
||||
"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 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 terminal closed.": "Dieses Terminal wurde geschlossen.",
|
||||
"Thought": "Nachgedacht",
|
||||
"Time": "Uhrzeit",
|
||||
"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.",
|
||||
"Close": "Cerrar",
|
||||
"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",
|
||||
"Command": "Comando",
|
||||
"Commands": "Comandos",
|
||||
@@ -739,12 +784,18 @@ const LEMBAS_I18N = {
|
||||
"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.",
|
||||
"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 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…",
|
||||
"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 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.",
|
||||
"Cut short here; the whole of it is in the web interface.": "Recortado aquí; completo está en la interfaz web.",
|
||||
"Days": "Días",
|
||||
@@ -753,6 +804,7 @@ const LEMBAS_I18N = {
|
||||
"Delete this chat?": "¿Eliminar este chat?",
|
||||
"Delete this scheduled task?": "¿Eliminar esta tarea programada?",
|
||||
"Deny": "Denegar",
|
||||
"Disconnected. Close and reopen to reconnect.": "Desconectado. Cierre y vuelva a abrir para reconectar.",
|
||||
"Done": "Hecho",
|
||||
"Drop the files to attach them": "Suelte los archivos para adjuntarlos",
|
||||
"Due %(when)s": "Prevista %(when)s",
|
||||
@@ -827,12 +879,14 @@ const LEMBAS_I18N = {
|
||||
"Not measured.": "Sin medir.",
|
||||
"Not run yet": "Aún no se ha ejecutado",
|
||||
"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 is scheduled.": "No hay nada programado.",
|
||||
"Nothing matches.": "No hay coincidencias.",
|
||||
"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 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.",
|
||||
"Notifications": "Notificaciones",
|
||||
"One chat that grows": "Un chat que va creciendo",
|
||||
@@ -854,9 +908,11 @@ const LEMBAS_I18N = {
|
||||
"Plan — changes nothing": "Plan — no cambia nada",
|
||||
"Preview": "Vista previa",
|
||||
"Project folders": "Carpetas de proyectos",
|
||||
"Put the last command and its output into the message box": "Poner el último comando y su salida en el cuadro de mensaje",
|
||||
"Read again": "Leer de nuevo",
|
||||
"Reasoning effort": "Esfuerzo de razonamiento",
|
||||
"Recommended": "Recomendado",
|
||||
"Reconnecting…": "Reconectando…",
|
||||
"Regenerate reply": "Regenerar respuesta",
|
||||
"Remove": "Quitar",
|
||||
"Remove %(dir)s": "Quitar %(dir)s",
|
||||
@@ -882,6 +938,7 @@ const LEMBAS_I18N = {
|
||||
"Search": "Buscar",
|
||||
"Search chats": "Buscar en los chats",
|
||||
"Send": "Enviar",
|
||||
"Send to chat": "Enviar al chat",
|
||||
"Sending…": "Enviando…",
|
||||
"Server": "Servidor",
|
||||
"Set the time yourself": "Fijar la hora usted mismo",
|
||||
@@ -890,6 +947,7 @@ const LEMBAS_I18N = {
|
||||
"Shown by the agent": "Mostrado por el agente",
|
||||
"Sidebar": "Barra lateral",
|
||||
"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",
|
||||
"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.",
|
||||
@@ -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",
|
||||
"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",
|
||||
"Terminal": "Terminal",
|
||||
"The agent asks to run %(tool)s:": "El agente pide ejecutar %(tool)s:",
|
||||
"The agent asks:": "El agente pregunta:",
|
||||
"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 model's own": "La del modelo",
|
||||
"The most an agent chat may do here": "Lo máximo que un chat de agente puede hacer aquí",
|
||||
"The next command": "El comando siguiente",
|
||||
"The next command (Ctrl+Shift+↓)": "El comando siguiente (Ctrl+Shift+↓)",
|
||||
"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 previous command": "El comando anterior",
|
||||
"The previous command (Ctrl+Shift+↑)": "El comando anterior (Ctrl+Shift+↑)",
|
||||
"The reply is ready.": "La respuesta está lista.",
|
||||
"The result is no longer there.": "El resultado ya no está.",
|
||||
"The server refused this computer's link: %(why)s": "El servidor rechazó la vinculación de este equipo: %(why)s",
|
||||
"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.",
|
||||
"Think before answering": "Razonar antes de responder",
|
||||
"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 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 terminal closed.": "Este terminal se ha cerrado.",
|
||||
"Thought": "Pensamiento",
|
||||
"Time": "Hora",
|
||||
"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.",
|
||||
"Close": "Fermer",
|
||||
"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é",
|
||||
"Command": "Commande",
|
||||
"Commands": "Commandes",
|
||||
@@ -1070,12 +1137,18 @@ const LEMBAS_I18N = {
|
||||
"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.",
|
||||
"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 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…",
|
||||
"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 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.",
|
||||
"Cut short here; the whole of it is in the web interface.": "Tronqué ici ; le tout est dans l'interface web.",
|
||||
"Days": "Jours",
|
||||
@@ -1084,6 +1157,7 @@ const LEMBAS_I18N = {
|
||||
"Delete this chat?": "Supprimer cette conversation ?",
|
||||
"Delete this scheduled task?": "Supprimer cette tâche planifiée ?",
|
||||
"Deny": "Refuser",
|
||||
"Disconnected. Close and reopen to reconnect.": "Déconnecté. Fermez et rouvrez pour vous reconnecter.",
|
||||
"Done": "Terminé",
|
||||
"Drop the files to attach them": "Déposez les fichiers pour les joindre",
|
||||
"Due %(when)s": "Prévue %(when)s",
|
||||
@@ -1158,12 +1232,14 @@ const LEMBAS_I18N = {
|
||||
"Not measured.": "Non mesuré.",
|
||||
"Not run yet": "Pas encore exécutée",
|
||||
"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 is scheduled.": "Rien n’est planifié.",
|
||||
"Nothing matches.": "Aucun résultat.",
|
||||
"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 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.",
|
||||
"Notifications": "Notifications",
|
||||
"One chat that grows": "Une conversation qui grandit",
|
||||
@@ -1185,9 +1261,11 @@ const LEMBAS_I18N = {
|
||||
"Plan — changes nothing": "Plan — ne change rien",
|
||||
"Preview": "Aperçu",
|
||||
"Project folders": "Dossiers de projets",
|
||||
"Put the last command and its output into the message box": "Mettre la dernière commande et sa sortie dans la zone de message",
|
||||
"Read again": "Relire",
|
||||
"Reasoning effort": "Effort de raisonnement",
|
||||
"Recommended": "Recommandé",
|
||||
"Reconnecting…": "Reconnexion…",
|
||||
"Regenerate reply": "Régénérer la réponse",
|
||||
"Remove": "Retirer",
|
||||
"Remove %(dir)s": "Retirer %(dir)s",
|
||||
@@ -1213,6 +1291,7 @@ const LEMBAS_I18N = {
|
||||
"Search": "Rechercher",
|
||||
"Search chats": "Rechercher dans les conversations",
|
||||
"Send": "Envoyer",
|
||||
"Send to chat": "Envoyer à la conversation",
|
||||
"Sending…": "Envoi…",
|
||||
"Server": "Serveur",
|
||||
"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",
|
||||
"Sidebar": "Barre latérale",
|
||||
"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",
|
||||
"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.",
|
||||
@@ -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",
|
||||
"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",
|
||||
"Terminal": "Terminal",
|
||||
"The agent asks to run %(tool)s:": "L'agent demande à exécuter %(tool)s :",
|
||||
"The agent asks:": "L'agent demande :",
|
||||
"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 model's own": "Celle du modèle",
|
||||
"The most an agent chat may do here": "Le maximum qu'une conversation d'agent peut faire ici",
|
||||
"The next command": "La commande suivante",
|
||||
"The next command (Ctrl+Shift+↓)": "La commande suivante (Ctrl+Shift+↓)",
|
||||
"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 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 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 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.",
|
||||
"Think before answering": "Réfléchir avant de répondre",
|
||||
"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 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 terminal closed.": "Ce terminal s'est fermé.",
|
||||
"Thought": "Réflexion",
|
||||
"Time": "Heure",
|
||||
"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.",
|
||||
"Close": "Bezárás",
|
||||
"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",
|
||||
"Command": "Parancs",
|
||||
"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 by itself once the window is known.": "Magától tömörül, amint ismert az ablak.",
|
||||
"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 and background tasks": "Kontextus és háttérfeladatok",
|
||||
"Continue in a terminal": "Folytatás terminálban",
|
||||
"Continue in a terminal…": "Folytatás terminálban…",
|
||||
"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 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.",
|
||||
"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",
|
||||
@@ -1415,6 +1510,7 @@ const LEMBAS_I18N = {
|
||||
"Delete this chat?": "Törli ezt a csevegést?",
|
||||
"Delete this scheduled task?": "Törli ezt az ütemezett feladatot?",
|
||||
"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",
|
||||
"Drop the files to attach them": "Engedje el a fájlokat a csatoláshoz",
|
||||
"Due %(when)s": "Esedékes: %(when)s",
|
||||
@@ -1489,12 +1585,14 @@ const LEMBAS_I18N = {
|
||||
"Not measured.": "Nincs mérve.",
|
||||
"Not run yet": "Még nem futott",
|
||||
"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 is scheduled.": "Nincs semmi ütemezve.",
|
||||
"Nothing matches.": "Nincs találat.",
|
||||
"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 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.",
|
||||
"Notifications": "Értesítések",
|
||||
"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",
|
||||
"Preview": "Előnézet",
|
||||
"Project folders": "Projektmappák",
|
||||
"Put the last command and its output into the message box": "Az utolsó parancs és kimenetének beillesztése az üzenetmezőbe",
|
||||
"Read again": "Újraolvasás",
|
||||
"Reasoning effort": "Gondolkodási erőfeszítés",
|
||||
"Recommended": "Ajánlott",
|
||||
"Reconnecting…": "Újrakapcsolódás…",
|
||||
"Regenerate reply": "Válasz újragenerálása",
|
||||
"Remove": "Eltávolítás",
|
||||
"Remove %(dir)s": "%(dir)s eltávolítása",
|
||||
@@ -1544,6 +1644,7 @@ const LEMBAS_I18N = {
|
||||
"Search": "Keresés",
|
||||
"Search chats": "Keresés a beszélgetésekben",
|
||||
"Send": "Küldés",
|
||||
"Send to chat": "Küldés a csevegésbe",
|
||||
"Sending…": "Küldés…",
|
||||
"Server": "Kiszolgáló",
|
||||
"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",
|
||||
"Sidebar": "Oldalsáv",
|
||||
"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",
|
||||
"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.",
|
||||
@@ -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",
|
||||
"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",
|
||||
"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:": "Az ügynök kérdezi:",
|
||||
"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 model's own": "A modell sajátja",
|
||||
"The most an agent chat may do here": "A legtöbb, amit egy ügynök-csevegés itt tehet",
|
||||
"The next command": "Következő parancs",
|
||||
"The next command (Ctrl+Shift+↓)": "Következő parancs (Ctrl+Shift+↓)",
|
||||
"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 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 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 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.",
|
||||
"Think before answering": "Gondolkodjon válasz előtt",
|
||||
"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 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 terminal closed.": "Ez a terminál bezárult.",
|
||||
"Thought": "Gondolkodott",
|
||||
"Time": "Időpont",
|
||||
"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.",
|
||||
"Close": "Chiudi",
|
||||
"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à",
|
||||
"Command": "Comando",
|
||||
"Commands": "Comandi",
|
||||
@@ -1732,12 +1843,18 @@ const LEMBAS_I18N = {
|
||||
"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.",
|
||||
"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 and background tasks": "Contesto e attività in background",
|
||||
"Continue in a terminal": "Continua in un terminale",
|
||||
"Continue in a terminal…": "Continua in un terminale…",
|
||||
"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 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.",
|
||||
"Cut short here; the whole of it is in the web interface.": "Troncato qui; il resto è nell'interfaccia web.",
|
||||
"Days": "Giorni",
|
||||
@@ -1746,6 +1863,7 @@ const LEMBAS_I18N = {
|
||||
"Delete this chat?": "Eliminare questa chat?",
|
||||
"Delete this scheduled task?": "Eliminare questa attività pianificata?",
|
||||
"Deny": "Nega",
|
||||
"Disconnected. Close and reopen to reconnect.": "Disconnesso. Chiudi e riapri per riconnetterti.",
|
||||
"Done": "Fatto",
|
||||
"Drop the files to attach them": "Rilasci i file per allegarli",
|
||||
"Due %(when)s": "Prevista %(when)s",
|
||||
@@ -1820,12 +1938,14 @@ const LEMBAS_I18N = {
|
||||
"Not measured.": "Non misurato.",
|
||||
"Not run yet": "Non ancora eseguita",
|
||||
"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 is scheduled.": "Non c’è niente di pianificato.",
|
||||
"Nothing matches.": "Nessun risultato.",
|
||||
"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 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.",
|
||||
"Notifications": "Notifiche",
|
||||
"One chat that grows": "Una chat che cresce",
|
||||
@@ -1847,9 +1967,11 @@ const LEMBAS_I18N = {
|
||||
"Plan — changes nothing": "Plan — non cambia nulla",
|
||||
"Preview": "Anteprima",
|
||||
"Project folders": "Cartelle dei progetti",
|
||||
"Put the last command and its output into the message box": "Metti l'ultimo comando e il suo output nella casella del messaggio",
|
||||
"Read again": "Rileggi",
|
||||
"Reasoning effort": "Sforzo di ragionamento",
|
||||
"Recommended": "Consigliato",
|
||||
"Reconnecting…": "Riconnessione…",
|
||||
"Regenerate reply": "Rigenera risposta",
|
||||
"Remove": "Rimuovi",
|
||||
"Remove %(dir)s": "Rimuovi %(dir)s",
|
||||
@@ -1875,6 +1997,7 @@ const LEMBAS_I18N = {
|
||||
"Search": "Cerca",
|
||||
"Search chats": "Cerca nelle chat",
|
||||
"Send": "Invia",
|
||||
"Send to chat": "Invia alla chat",
|
||||
"Sending…": "Invio…",
|
||||
"Server": "Server",
|
||||
"Set the time yourself": "Imposta tu l’orario",
|
||||
@@ -1883,6 +2006,7 @@ const LEMBAS_I18N = {
|
||||
"Shown by the agent": "Mostrato dall'agente",
|
||||
"Sidebar": "Barra laterale",
|
||||
"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",
|
||||
"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.",
|
||||
@@ -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",
|
||||
"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",
|
||||
"Terminal": "Terminale",
|
||||
"The agent asks to run %(tool)s:": "L'agente chiede di eseguire %(tool)s:",
|
||||
"The agent asks:": "L'agente chiede:",
|
||||
"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 model's own": "Quello del modello",
|
||||
"The most an agent chat may do here": "Il massimo che una chat agente può fare qui",
|
||||
"The next command": "Il comando successivo",
|
||||
"The next command (Ctrl+Shift+↓)": "Il comando successivo (Ctrl+Shift+↓)",
|
||||
"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 previous command": "Il comando precedente",
|
||||
"The previous command (Ctrl+Shift+↑)": "Il comando precedente (Ctrl+Shift+↑)",
|
||||
"The reply is ready.": "La risposta è pronta.",
|
||||
"The result is no longer there.": "Il risultato non c’è più.",
|
||||
"The server refused this computer's link: %(why)s": "Il server ha rifiutato il collegamento di questo computer: %(why)s",
|
||||
"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.",
|
||||
"Think before answering": "Ragiona prima di rispondere",
|
||||
"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 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 terminal closed.": "Questo terminale si è chiuso.",
|
||||
"Thought": "Ha ragionato",
|
||||
"Time": "Ora",
|
||||
"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.",
|
||||
"Close": "Zamknij",
|
||||
"Close (LLeMbas stays in the tray)": "Zamknij (LLeMbas zostaje w zasobniku systemowym)",
|
||||
"Close terminal": "Zamknij terminal",
|
||||
"Close the activity panel": "Zamknij panel aktywności",
|
||||
"Command": "Polecenie",
|
||||
"Commands": "Polecenia",
|
||||
@@ -2063,12 +2196,18 @@ const LEMBAS_I18N = {
|
||||
"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.",
|
||||
"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 and background tasks": "Kontekst i zadania w tle",
|
||||
"Continue in a terminal": "Kontynuuj w terminalu",
|
||||
"Continue in a terminal…": "Kontynuuj w terminalu…",
|
||||
"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 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.",
|
||||
"Cut short here; the whole of it is in the web interface.": "Tu skrócone; całość jest w interfejsie WWW.",
|
||||
"Days": "Dni",
|
||||
@@ -2077,6 +2216,7 @@ const LEMBAS_I18N = {
|
||||
"Delete this chat?": "Usunąć ten czat?",
|
||||
"Delete this scheduled task?": "Usunąć to zaplanowane zadanie?",
|
||||
"Deny": "Odrzuć",
|
||||
"Disconnected. Close and reopen to reconnect.": "Rozłączono. Aby połączyć ponownie, zamknij i otwórz ponownie.",
|
||||
"Done": "Gotowe",
|
||||
"Drop the files to attach them": "Upuść pliki, aby je dołączyć",
|
||||
"Due %(when)s": "Zaplanowane na %(when)s",
|
||||
@@ -2151,12 +2291,14 @@ const LEMBAS_I18N = {
|
||||
"Not measured.": "Nie zmierzono.",
|
||||
"Not run yet": "Jeszcze nie uruchomione",
|
||||
"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 is scheduled.": "Nic nie jest zaplanowane.",
|
||||
"Nothing matches.": "Nic nie pasuje.",
|
||||
"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 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.",
|
||||
"Notifications": "Powiadomienia",
|
||||
"One chat that grows": "Jeden czat, który rośnie",
|
||||
@@ -2178,9 +2320,11 @@ const LEMBAS_I18N = {
|
||||
"Plan — changes nothing": "Plan — niczego nie zmienia",
|
||||
"Preview": "Podgląd",
|
||||
"Project folders": "Foldery projektów",
|
||||
"Put the last command and its output into the message box": "Wstaw ostatnie polecenie i jego wynik do pola wiadomości",
|
||||
"Read again": "Wczytaj ponownie",
|
||||
"Reasoning effort": "Poziom rozumowania",
|
||||
"Recommended": "Zalecane",
|
||||
"Reconnecting…": "Ponowne łączenie…",
|
||||
"Regenerate reply": "Wygeneruj odpowiedź ponownie",
|
||||
"Remove": "Usuń",
|
||||
"Remove %(dir)s": "Usuń %(dir)s",
|
||||
@@ -2206,6 +2350,7 @@ const LEMBAS_I18N = {
|
||||
"Search": "Szukaj",
|
||||
"Search chats": "Szukaj w czatach",
|
||||
"Send": "Wyślij",
|
||||
"Send to chat": "Wyślij do czatu",
|
||||
"Sending…": "Wysyłanie…",
|
||||
"Server": "Serwer",
|
||||
"Set the time yourself": "Ustaw czas samodzielnie",
|
||||
@@ -2214,6 +2359,7 @@ const LEMBAS_I18N = {
|
||||
"Shown by the agent": "Pokazane przez agenta",
|
||||
"Sidebar": "Pasek boczny",
|
||||
"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ę",
|
||||
"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.",
|
||||
@@ -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",
|
||||
"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",
|
||||
"Terminal": "Terminal",
|
||||
"The agent asks to run %(tool)s:": "Agent prosi o uruchomienie %(tool)s:",
|
||||
"The agent asks:": "Agent pyta:",
|
||||
"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 model's own": "Domyślne modelu",
|
||||
"The most an agent chat may do here": "Najwięcej, co czat agenta może tu zrobić",
|
||||
"The next command": "Następne polecenie",
|
||||
"The next command (Ctrl+Shift+↓)": "Następne polecenie (Ctrl+Shift+↓)",
|
||||
"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 previous command": "Poprzednie polecenie",
|
||||
"The previous command (Ctrl+Shift+↑)": "Poprzednie polecenie (Ctrl+Shift+↑)",
|
||||
"The reply is ready.": "Odpowiedź jest gotowa.",
|
||||
"The result is no longer there.": "Wyniku już nie ma.",
|
||||
"The server refused this computer's link: %(why)s": "Serwer odrzucił połączenie tego komputera: %(why)s",
|
||||
"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.",
|
||||
"Think before answering": "Rozumuj przed odpowiedzią",
|
||||
"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 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 terminal closed.": "Ten terminal został zamknięty.",
|
||||
"Thought": "Rozumowanie",
|
||||
"Time": "Godzina",
|
||||
"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ť.",
|
||||
"Close": "Zavrieť",
|
||||
"Close (LLeMbas stays in the tray)": "Zavrieť (LLeMbas zostane v oznamovacej oblasti)",
|
||||
"Close terminal": "Zavrieť terminál",
|
||||
"Close the activity panel": "Zavrieť panel aktivity",
|
||||
"Command": "Príkaz",
|
||||
"Commands": "Príkazy",
|
||||
@@ -2394,12 +2549,18 @@ const LEMBAS_I18N = {
|
||||
"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.",
|
||||
"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 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…",
|
||||
"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 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é.",
|
||||
"Cut short here; the whole of it is in the web interface.": "Tu skrátené; celé je to vo webovom rozhraní.",
|
||||
"Days": "Dni",
|
||||
@@ -2408,6 +2569,7 @@ const LEMBAS_I18N = {
|
||||
"Delete this chat?": "Zmazať túto konverzáciu?",
|
||||
"Delete this scheduled task?": "Odstrániť túto naplánovanú úlohu?",
|
||||
"Deny": "Odmietnuť",
|
||||
"Disconnected. Close and reopen to reconnect.": "Odpojené. Zavrite a znova otvorte, aby sa pripojilo.",
|
||||
"Done": "Hotovo",
|
||||
"Drop the files to attach them": "Pustite súbory a priložia sa",
|
||||
"Due %(when)s": "Naplánované na %(when)s",
|
||||
@@ -2482,12 +2644,14 @@ const LEMBAS_I18N = {
|
||||
"Not measured.": "Nezmerané.",
|
||||
"Not run yet": "Ešte nebežala",
|
||||
"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 is scheduled.": "Nič nie je naplánované.",
|
||||
"Nothing matches.": "Nič nezodpovedá.",
|
||||
"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 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č.",
|
||||
"Notifications": "Upozornenia",
|
||||
"One chat that grows": "Jedna konverzácia, ktorá rastie",
|
||||
@@ -2509,9 +2673,11 @@ const LEMBAS_I18N = {
|
||||
"Plan — changes nothing": "Plan — nič nemení",
|
||||
"Preview": "Náhľad",
|
||||
"Project folders": "Priečinky projektov",
|
||||
"Put the last command and its output into the message box": "Vložiť posledný príkaz a jeho výstup do políčka správy",
|
||||
"Read again": "Načítať znova",
|
||||
"Reasoning effort": "Náročnosť uvažovania",
|
||||
"Recommended": "Odporúčané",
|
||||
"Reconnecting…": "Znova sa pripája…",
|
||||
"Regenerate reply": "Vygenerovať odpoveď znova",
|
||||
"Remove": "Odstrániť",
|
||||
"Remove %(dir)s": "Odstrániť %(dir)s",
|
||||
@@ -2537,6 +2703,7 @@ const LEMBAS_I18N = {
|
||||
"Search": "Hľadať",
|
||||
"Search chats": "Hľadať v konverzáciách",
|
||||
"Send": "Poslať",
|
||||
"Send to chat": "Poslať do konverzácie",
|
||||
"Sending…": "Odosiela sa…",
|
||||
"Server": "Server",
|
||||
"Set the time yourself": "Nastaviť čas sami",
|
||||
@@ -2545,6 +2712,7 @@ const LEMBAS_I18N = {
|
||||
"Shown by the agent": "Ukázal agent",
|
||||
"Sidebar": "Bočný panel",
|
||||
"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",
|
||||
"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.",
|
||||
@@ -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",
|
||||
"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",
|
||||
"Terminal": "Terminál",
|
||||
"The agent asks to run %(tool)s:": "Agent žiada spustiť %(tool)s:",
|
||||
"The agent asks:": "Agent sa pýta:",
|
||||
"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 model's own": "Podľa modelu",
|
||||
"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 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 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 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.",
|
||||
"Think before answering": "Premýšľať pred odpoveďou",
|
||||
"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 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 terminal closed.": "Tento terminál sa zatvoril.",
|
||||
"Thought": "Premýšľal",
|
||||
"Time": "Čas",
|
||||
"Times in %(zone)s": "Časové pásmo: %(zone)s",
|
||||
|
||||
@@ -135,6 +135,26 @@
|
||||
</div>
|
||||
</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">
|
||||
<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>
|
||||
|
||||
@@ -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. */
|
||||
export function openLink(url) {
|
||||
return invoke("open_external", { url }).catch(() => {})
|
||||
|
||||
+59
-11
@@ -18,10 +18,11 @@ import { readyIds, sending, sent, discard } from "./attach.js"
|
||||
import { watchActivity, activityChanged, planList, activityButton } from "./activity.js"
|
||||
import { agentBlock, agentReady, agentState, onAgentChange } from "./agent.js"
|
||||
import { folderName } from "./list.js"
|
||||
import { aimTerminal, terminalButton, terminalOpen, toggleTerminal } from "./terminal.js"
|
||||
|
||||
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 }
|
||||
|
||||
export function openChatId() {
|
||||
@@ -34,6 +35,44 @@ export function onThisComputer(chat) {
|
||||
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() {
|
||||
const th = $("thread")
|
||||
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") }),
|
||||
),
|
||||
)
|
||||
if (chat.device_session_id) {
|
||||
tools.append(
|
||||
h(
|
||||
"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),
|
||||
),
|
||||
)
|
||||
}
|
||||
// The Terminal panel where the chat's device opens one (`GET /devices` says so per device);
|
||||
// "Continue in a terminal" stays in the chat's menu.
|
||||
if (deviceOf(chat.device.token_id)?.terminal) tools.append(terminalButton())
|
||||
}
|
||||
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)))
|
||||
@@ -751,6 +784,8 @@ async function renderChips() {
|
||||
parts.push(folderChip(roots, draft.dir, (d) => ((draft.dir = d), renderChips())))
|
||||
parts.push(modeChip(draft.mode, (m) => ((draft.mode = m), renderChips())))
|
||||
}
|
||||
const shell = terminalTarget()
|
||||
aimTerminal(shell)
|
||||
parts.push(modelChip(models, draft.model, (id) => {
|
||||
draft.model = id
|
||||
draft.effort = levelsOf(models.find((m) => m.id === id)).start
|
||||
@@ -758,10 +793,19 @@ async function renderChips() {
|
||||
}, mayPick))
|
||||
if (model) parts.push(effortChip(model, draft.effort, (l) => ((draft.effort = l), renderChips())))
|
||||
if (draft.kind === "chat" && teamPossible(model)) parts.push(agentsChip(draft.agents, (a) => ((draft.agents = a), renderChips()), { cap: subagentsCap(), own: namedOnly(model?.subagents) }))
|
||||
// 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))
|
||||
return
|
||||
}
|
||||
const chat = open?.chat
|
||||
aimTerminal(terminalTarget())
|
||||
if (!chat?.kind) return box.replaceChildren()
|
||||
const save = async (body) => {
|
||||
try {
|
||||
@@ -809,6 +853,10 @@ export function statusChanged() {
|
||||
if (draft) {
|
||||
renderChips()
|
||||
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
|
||||
if (agents && (agents.min || agents.max)) body.agents = agents
|
||||
if (draft.kind === "agent") {
|
||||
const device = ctx.thisDevices()[0]
|
||||
const device = draftDevice()
|
||||
if (!device || !agentReady(ctx.status())) {
|
||||
renderNotice()
|
||||
throw { detail: agentState(ctx.status()).sentence }
|
||||
|
||||
@@ -76,6 +76,7 @@ export const ICONS = {
|
||||
max: "M4 4h8v8H4zM5.5 5.5v5h5v-5z",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
|
||||
+19
-3
@@ -12,7 +12,8 @@ import { renderSettings, initSettings, scrollToSection } from "./settings.js"
|
||||
import { initViewer, onViewer, closeViewer } from "./viewer.js"
|
||||
import { initExplorer, showLocal, showFilesTab } from "./explorer.js"
|
||||
import { initList, refreshList, markCurrent } from "./list.js"
|
||||
import { initChat, openChat, newChat, closeChat, openChatId, chatMenu, statusChanged, draftKind, 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 { initPalette, openPalette } from "./palette.js"
|
||||
import { initActivity, showPanel, togglePanel, panelOpen } from "./activity.js"
|
||||
@@ -67,6 +68,7 @@ function applyTheme(name) {
|
||||
const root = document.documentElement
|
||||
root.dataset.theme = name
|
||||
root.dataset.base = name
|
||||
terminalThemeChanged()
|
||||
}
|
||||
|
||||
// --- Views ------------------------------------------------------------------------------------
|
||||
@@ -162,7 +164,10 @@ function showPage(name) {
|
||||
$("page-scheduled").hidden = name !== "scheduled"
|
||||
$("settings").hidden = name !== "settings"
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -221,6 +226,7 @@ function commands() {
|
||||
{ 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 (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 }) })
|
||||
out.push(
|
||||
{ label: status?.linked ? t("Link again") : t("Link this computer"), run: link },
|
||||
@@ -265,6 +271,7 @@ async function boot() {
|
||||
initChat({
|
||||
status: () => status,
|
||||
thisDevices,
|
||||
devices: () => devices,
|
||||
models: () => models,
|
||||
me: () => me,
|
||||
changed: () => refreshList(),
|
||||
@@ -289,9 +296,18 @@ async function boot() {
|
||||
onViewer((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({
|
||||
onLayout: (on) => {
|
||||
$("shell").dataset.activity = String(on)
|
||||
if (on && terminalOpen()) showTerminal(false)
|
||||
},
|
||||
here: onThisComputer,
|
||||
// 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)
|
||||
route()
|
||||
// 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", () => {
|
||||
if (view === "app") showSettings(true)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
Vendored
+2
@@ -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
|
||||
Vendored
+218
@@ -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;
|
||||
}
|
||||
Vendored
+2
@@ -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
|
||||
Reference in new issue
Block a user