Files in the web explorer's shape: crumbs, folder tree, list in columns, details and preview
ci / ui (push) Waiting to run
ci / check (push) Failing after 3s
ci / start (push) Failing after 3s

Both tabs use the window's width now, as the web's explorer does: where you are (crumbs), the
folder tree (a chevron opens, a name lists), the list on one grid with every row a subgrid --
name with an icon by kind, type, size, changed, a menu -- and on a wide window the chosen file's
details, the start of it and Open in the viewer (a double click opens it). The panes give way by
the room the page has (container queries, so the sidebar's share counts): the details at 60rem,
the tree at 40rem, the type and date columns when the list is under 28rem; a file then opens in
the viewer on a click. The tree and the list share one request per folder; every reading keeps
the never-empty states of the previous commit.

Tests (tests/ui FX): the administrator's / wide (tree, columns on one track, details, preview,
viewer), navigating by list, tree and crumbs with a long name, this computer's folder, and every
width 1920/1280/1024/800/390 with nothing sideways, every control inside the window and the
name's track never closed. H8/N8 follow the new list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-10 16:18:05 +00:00
1 parent 6874dd46cd
commit fe4bf064f5
9 files changed
+987 -227

No files matched your search

+189 -40
View File
@@ -1414,57 +1414,206 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
}
.tabs__tab[aria-selected="true"] { color: var(--ink); background: var(--surface-active); }
.files { display: grid; align-content: start; }
.files__pane { display: grid; gap: var(--sp-2); background: var(--surface); border: var(--border-w) solid var(--border); border-radius: var(--radius-soft); padding: var(--sp-3); min-width: 0; }
.files__head { display: grid; grid-template-columns: minmax(0, 1fr) var(--control-h-sm); gap: var(--sp-2); align-items: center; }
.files__where { min-width: 0; }
/* The server's pane: Upload and New folder before Read again. */
#pane-server .files__head { grid-template-columns: minmax(0, 1fr) repeat(3, var(--control-h-sm)); }
#server-status { white-space: pre-line; }
.files__where select { width: 100%; height: var(--control-h-sm); }
.files__tree { min-height: 0; }
.pane__path { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; direction: rtl; text-align: left; font: var(--text-xs) var(--font-ui); color: var(--ink-muted); }
.pane__pick { width: 100%; }
.pane__note { padding: var(--sp-2) 0; font-size: var(--text-sm); color: var(--ink-muted); }
.files__tree > .btn { justify-self: start; }
.tree, .tree ul { list-style: none; margin: 0; padding: 0; }
.tree__row {
width: 100%;
height: var(--control-h-sm);
/* --- Files: two explorers in the web's shape ------------------------------------------------
The page's whole width under its head, like a file manager: where you are and what can be done
there, then the folder tree, the list and the chosen file's details side by side, each its own
scroller. The list is one grid with a track per column and every row a subgrid of it, so a long
name never pushes the type, size or date out of line: the name's track is the one that gives.
Narrower, the details go first (a file then opens in the viewer), then the tree (the crumbs
are the way up), then the type and date columns. */
.page__body.files { display: grid; grid-template-rows: minmax(0, 1fr); padding: 0; overflow: hidden; }
.page__body.files > * { max-width: none; margin: 0; }
.fx { display: grid; grid-template-rows: auto auto minmax(0, 1fr); min-width: 0; min-height: 0; container: fx / inline-size; }
.fx > .fx__bar { grid-row: 1; }
.fx > .fx__status { grid-row: 2; }
.fx > .fx__panes { grid-row: 3; }
.fx[hidden] { display: none; }
.fx__bar {
display: grid;
grid-template-columns: calc(var(--depth, 0) * var(--sp-4)) 1rem 1rem minmax(0, 1fr) auto;
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-areas: "where crumbs tools";
align-items: center;
gap: var(--sp-2);
min-height: var(--header-height);
padding: var(--sp-1) var(--sp-4);
border-bottom: var(--border-w) solid var(--border);
background: var(--surface);
}
.fx__where { grid-area: where; min-width: 0; max-width: 18rem; }
.fx__bar > .crumbs { grid-area: crumbs; }
.fx__where:empty { display: none; }
.fx__tools { grid-area: tools; display: flex; align-items: center; gap: var(--sp-1); }
.fx__status { margin: 0; padding: var(--sp-2) var(--sp-4); border-bottom: var(--border-w) solid var(--border); white-space: pre-line; }
.fx__status[hidden] { display: none; }
.pane__pick { width: 100%; height: var(--control-h-sm); }
.crumbs {
display: flex;
align-items: center;
gap: var(--sp-1);
min-width: 0;
overflow-x: auto;
white-space: nowrap;
scrollbar-width: none;
font: var(--text-sm) var(--font-mono);
}
.crumbs__sep { color: var(--ink-faint); }
.crumbs__link {
min-height: var(--control-h-sm);
padding: 0 var(--sp-1);
font: inherit;
color: var(--ink-muted);
background: transparent;
border: 0;
border-radius: var(--radius-sm);
cursor: pointer;
}
.crumbs__link:hover { color: var(--ink); background: var(--surface-hover); }
.crumbs__here { padding: 0 var(--sp-1); color: var(--ink); font-weight: 600; }
.fx__panes { display: grid; grid-template-columns: minmax(11rem, 14rem) minmax(0, 1fr) minmax(14rem, 18rem); min-width: 0; min-height: 0; }
.fx__tree, .fx__list, .fx__details { min-width: 0; min-height: 0; overflow: auto; overscroll-behavior: contain; }
.fx__tree { padding: var(--sp-2); border-right: var(--border-w) solid var(--border); background: var(--surface); }
.fx__list { padding: 0 var(--sp-4) var(--sp-8); container: fxlist / inline-size; }
.fx__details { display: grid; align-content: start; gap: var(--sp-3); padding: var(--sp-4); border-left: var(--border-w) solid var(--border); background: var(--surface); }
.fx[data-empty="true"] .fx__tree, .fx[data-empty="true"] .fx__details { display: none; }
.fx[data-empty="true"] .fx__panes { grid-template-columns: minmax(0, 1fr); }
/* The folder tree: a chevron opens a folder, its name lists it. */
.ftree, .ftree ul { list-style: none; margin: 0; padding: 0; }
.ftree__line { display: grid; grid-template-columns: calc(var(--depth, 0) * var(--sp-3)) 1.25rem minmax(0, 1fr); align-items: center; }
.ftree__line::before { content: ""; }
.ftree__chevron { display: grid; place-items: center; width: 1.25rem; height: var(--control-h-sm); padding: 0; color: var(--ink-faint); background: transparent; border: 0; cursor: pointer; }
.ftree__chevron .icon { width: 12px; height: 12px; transition: transform var(--dur-1) var(--ease-out); }
.ftree__chevron[aria-expanded="true"] .icon { transform: rotate(90deg); }
.ftree__row {
display: grid;
grid-template-columns: 1rem minmax(0, 1fr);
gap: var(--sp-2);
align-items: center;
width: 100%;
min-height: var(--control-h-sm);
padding: 0 var(--sp-2);
text-align: left;
font: var(--text-sm) var(--font-body);
text-align: left;
color: var(--ink-muted);
background: transparent;
border: 0;
border-radius: var(--radius-sm);
cursor: pointer;
}
.ftree__row:hover { color: var(--ink); background: var(--surface-hover); }
.ftree__row.is-current { color: var(--ink); background: var(--surface-active); font-weight: 600; }
.ftree__root { margin-bottom: var(--sp-1); }
.ftree__name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
/* The list. */
.frows {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(3.5rem, auto) minmax(4.5rem, auto) minmax(6.5rem, auto) var(--control-h-sm);
column-gap: var(--sp-4);
}
.frow {
position: relative;
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
align-items: center;
min-height: var(--row-h);
margin: 0 calc(-1 * var(--sp-2));
padding: 0 var(--sp-2);
border-bottom: var(--border-w) solid var(--border);
border-radius: var(--radius-sm);
}
.frow:not(.frow--head):hover { background: var(--surface-hover); }
.frow.is-chosen { background: var(--surface-active); }
.frow--head {
position: sticky;
top: 0;
z-index: 1;
background: var(--bg);
color: var(--ink-faint);
font: var(--text-xs) var(--font-ui);
letter-spacing: 0.06em;
text-transform: uppercase;
border-radius: 0;
}
.frow__name { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.frow__open {
display: inline-flex;
align-items: center;
gap: var(--sp-3);
min-width: 0;
min-height: var(--row-h);
padding: 0;
font: var(--text-sm) var(--font-body);
text-align: left;
color: var(--ink);
background: transparent;
border: 0;
border-radius: var(--radius-lg);
cursor: pointer;
}
/* The indent's track needs an item of its own, or the chevron would sit in it. */
.tree__row::before { content: ""; }
.tree__row:hover { background: var(--surface-hover); }
.tree__row .icon { color: var(--ink-muted); }
.tree__chevron { display: grid; place-items: center; width: 1rem; }
.tree__chevron .icon { width: 12px; height: 12px; transition: transform var(--dur-1) var(--ease-out); }
.tree__row[aria-expanded="true"] .tree__chevron .icon { transform: rotate(90deg); }
.tree__name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.tree__size { font: var(--text-xs) var(--font-ui); color: var(--ink-faint); }
/* A row with a more button beside it: the button on the row's own line, its folder below both. */
.tree__item { display: grid; grid-template-columns: minmax(0, 1fr) var(--control-h-sm); align-items: center; }
.tree__item > ul { grid-column: 1 / -1; }
.tree__more { opacity: 0; }
.tree__item:hover > .tree__more, .tree__more:focus-visible, .tree__more[aria-expanded="true"] { opacity: 1; }
@media (hover: none) { .tree__more { opacity: 1; } }
.tree__note { padding: var(--sp-1) var(--sp-3); padding-inline-start: calc(var(--sp-3) + var(--depth, 0) * var(--sp-4)); font-size: var(--text-xs); color: var(--ink-muted); }
/* The whole row opens what it names; the buttons at its end stay on top. */
.frow__open::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.frow__open:focus-visible { outline: none; }
.frow__open:focus-visible::after { outline: var(--outline-w) solid var(--accent); outline-offset: calc(-1 * var(--outline-w)); }
.frow__name .badge, .frow__actions { position: relative; z-index: 1; }
.frow__icon { display: inline-flex; color: var(--ink-muted); }
.frow__icon--dir, .frow__icon--mount { color: var(--accent); }
.frow__icon--code, .frow__icon--html { color: var(--leaf); }
.frow__label { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.frow__type { font: var(--text-xs) var(--font-mono); color: var(--ink-faint); white-space: nowrap; }
.frow__size, .frow__when { font-size: var(--text-sm); color: var(--ink-muted); white-space: nowrap; text-align: right; }
.frow--head .frow__size, .frow--head .frow__when { font-size: var(--text-xs); }
.frow__actions { display: flex; justify-content: flex-end; }
.frow__more { opacity: 0; }
.frow:hover .frow__more, .frow__more:focus-visible, .frow__more[aria-expanded="true"] { opacity: 1; }
@media (hover: none) { .frow__more { opacity: 1; } }
.badge { padding: 0 var(--sp-1); font: var(--text-xs) var(--font-ui); color: var(--ink-muted); border: var(--border-w) solid var(--border); border-radius: var(--radius-sm); white-space: nowrap; }
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.fx__empty { display: grid; justify-items: center; gap: var(--sp-3); padding: var(--sp-12) var(--sp-4); text-align: center; color: var(--ink-muted); }
.fx__empty .icon { width: 24px; height: 24px; color: var(--leaf); }
/* The chosen file. */
.fdetails__none { display: grid; justify-items: center; gap: var(--sp-2); padding: var(--sp-8) 0; text-align: center; color: var(--ink-muted); font-size: var(--text-sm); }
.fdetails__none .icon { width: 24px; height: 24px; color: var(--ink-faint); }
.fdetails__id { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--sp-2); align-items: center; }
.fdetails__name { font: 600 var(--text-base) / var(--leading-tight) var(--font-body); overflow-wrap: anywhere; }
.fdetails__facts { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--sp-1) var(--sp-3); margin: 0; font-size: var(--text-sm); }
.fdetails__facts dt { color: var(--ink-faint); }
.fdetails__facts dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.fdetails__do { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.fdetails__preview { min-width: 0; }
.fdetails__text { margin: 0; max-height: 24rem; overflow: auto; padding: var(--sp-3); font: var(--text-xs) / var(--leading-normal) var(--font-mono); white-space: pre; color: var(--ink); background: var(--bg); border: var(--border-w) solid var(--border); border-radius: var(--radius-sm); }
.fdetails__note { font-size: var(--text-sm); color: var(--ink-muted); }
/* Loading, still waiting, refused: the words, then Try again on the same line when it fits. */
.tree__note.is-loading, .tree__note.is-error { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.tree__note > span { min-width: 0; overflow-wrap: anywhere; }
.tree__retry { flex: none; }
.fnote { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-1); padding-inline-start: calc(var(--sp-1) + var(--depth, 0) * var(--sp-3)); font-size: var(--text-sm); color: var(--ink-muted); }
.ftree .fnote { font-size: var(--text-xs); }
.fnote.is-error { color: var(--warning); }
.fnote > span { min-width: 0; overflow-wrap: anywhere; }
.fnote__retry { flex: none; }
/* By the room the page has, not the window's: the sidebar takes its share first. */
@container fx (max-width: 60rem) {
.fx__panes { grid-template-columns: minmax(10rem, 14rem) minmax(0, 1fr); }
.fx__details { display: none; }
}
@container fx (max-width: 40rem) {
.fx__panes { grid-template-columns: minmax(0, 1fr); }
.fx__tree { display: none; }
.fx__bar { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "where where" "crumbs tools"; padding: var(--sp-1) var(--sp-3); }
.fx__where { max-width: none; }
.fx__list { padding: 0 var(--sp-3) var(--sp-8); }
}
/* A list narrower than its columns keeps the name and the size; the type and date go. */
@container fxlist (max-width: 28rem) {
.frows { grid-template-columns: minmax(0, 1fr) auto var(--control-h-sm); column-gap: var(--sp-2); }
.frows .frow__type, .frows .frow__when { display: none; }
}
@media (max-width: 48rem) {
.frow, .frow__open { min-height: var(--tap-min); }
}
/* An empty page: an icon, a heading, a sentence, centred. */
.empty { display: grid; justify-items: center; gap: var(--sp-3); padding: var(--sp-12) var(--sp-4) var(--sp-4); text-align: center; }
+136
View File
@@ -47,6 +47,7 @@ const LEMBAS_I18N = {
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Úloha bez vlastního modelu běží na prvním volném z těchto, v tomto pořadí. Žádný nezaškrtnutý: všechny.",
"About": "O aplikaci",
"Account": "Účet",
"Actions": "Akce",
"Activity": "Aktivita",
"Add": "Přidat",
"Add folder…": "Přidat složku…",
@@ -54,6 +55,7 @@ const LEMBAS_I18N = {
"Agent": "Agent",
"Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Agentní konverzace mohou pracovat v těchto složkách a ve všem uvnitř nich — nikde jinde na tomto počítači.",
"Agent: %(why)s": "Agent: %(why)s",
"All files": "Všechny soubory",
"Allow": "Povolit",
"Always": "Vždy",
"Always allow": "Vždy povolit",
@@ -77,10 +79,12 @@ const LEMBAS_I18N = {
"Background tasks": "Úlohy na pozadí",
"Cancel": "Zrušit",
"Carry it out": "Provést to",
"Changed": "Změněno",
"Chat": "Konverzace",
"Chats": "Konverzace",
"Check for updates…": "Zkontrolovat aktualizace…",
"Choose": "Vybrat",
"Choose a file to see it here.": "Vyberte soubor a uvidíte ho tady.",
"Choose a folder first.": "Nejprve vyberte složku.",
"Choose a folder…": "Vybrat složku…",
"Choose a model to choose its effort.": "Vyberte model, abyste mohli zvolit jeho náročnost.",
@@ -101,6 +105,7 @@ const LEMBAS_I18N = {
"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…",
"Contents": "Obsah",
"Context": "Kontext",
"Context and background tasks": "Kontext a úlohy na pozadí",
"Context used": "Využitý kontext",
@@ -122,6 +127,7 @@ const LEMBAS_I18N = {
"Delete this chat?": "Smazat tuto konverzaci?",
"Delete this scheduled task?": "Odstranit tuto naplánovanou úlohu?",
"Deny": "Zamítnout",
"Details": "Podrobnosti",
"Dictate a message": "Nadiktovat zprávu",
"Disconnected. Close and reopen to reconnect.": "Odpojeno. Pro opětovné připojení zavřete a znovu otevřete.",
"Done": "Hotovo",
@@ -155,10 +161,12 @@ const LEMBAS_I18N = {
"Folder name": "Název složky",
"Folder on %(device)s": "Složka na zařízení %(device)s",
"Folder on the computer": "Složka na počítači",
"Folders": "Složky",
"Free space": "Volné místo",
"Hello": "Dobrý den",
"Hello, %(name)s": "Dobrý den, %(name)s",
"History": "Historie",
"Home": "Domů",
"How many work on it at once, the run's own agent among them, on the same model.": "Kolik jich na ní pracuje současně, včetně vlastního agenta běhu, na stejném modelu.",
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Vlastními slovy. Server z nich vytvoří pravidlo a pokyn a před uložením vám ukáže další spuštění.",
"Install LLeMbas %(version)s…": "Nainstalovat LLeMbas %(version)s…",
@@ -197,10 +205,12 @@ const LEMBAS_I18N = {
"Models": "Modely",
"Models for scheduled tasks": "Modely pro naplánované úlohy",
"More": "Více",
"More for %(name)s": "Další pro %(name)s",
"Move to folder…": "Přesunout do složky…",
"Move to the trash": "Přesunout do koše",
"Move “%(name)s” to the trash? It can be restored from there.": "Přesunout „%(name)s“ do koše? Odtamtud ho lze obnovit.",
"Moved to the trash.": "Přesunuto do koše.",
"Name": "Název",
"Never": "Nikdy",
"New agent chat": "Nová agentní konverzace",
"New chat": "Nová konverzace",
@@ -225,6 +235,7 @@ const LEMBAS_I18N = {
"Noted by a model": "Zaznamenal model",
"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 here yet. Not even a hobbit hole.": "Zatím tu nic není. Ani hobití nora.",
"Nothing is scheduled.": "Nic není naplánováno.",
"Nothing matches.": "Nic neodpovídá.",
"Nothing remembered yet.": "Zatím si nic nepamatuje.",
@@ -250,6 +261,7 @@ const LEMBAS_I18N = {
"Or type an answer": "Nebo napište odpověď",
"Outputs": "Výstupy",
"Page": "Stránka",
"Path": "Cesta",
"Pause": "Pozastavit",
"Pause %(title)s": "Pozastavit %(title)s",
"Paused": "Pozastaveno",
@@ -316,6 +328,7 @@ const LEMBAS_I18N = {
"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.",
"Since the last reply": "Od poslední odpovědi",
"Size": "Velikost",
"Skills": "Dovednosti",
"Skipped": "Vynecháno",
"Something to remember…": "Něco k zapamatování…",
@@ -412,6 +425,7 @@ const LEMBAS_I18N = {
"Tools": "Nástroje",
"Tools and skills…": "Nástroje a dovednosti…",
"Try again": "Zkusit znovu",
"Type": "Typ",
"Type a command or a chat's title": "Napište příkaz nebo název konverzace",
"Type a folder…": "Zadat složku…",
"Unpin chat": "Odepnout konverzaci",
@@ -433,6 +447,7 @@ const LEMBAS_I18N = {
"When": "Kdy",
"When it matters": "Když na tom záleží",
"Where it runs": "Kde se spouští",
"Where you are": "Kde jste",
"Wide": "Široký",
"Writing": "Píše",
"Writing down…": "Zapisuji…",
@@ -468,6 +483,7 @@ const LEMBAS_I18N = {
"failed": "selhalo",
"findings: %(n)s": "zjištění: %(n)s",
"finished": "dokončený",
"folder": "složka",
"free": "volný",
"idle": "nečinný",
"it serves one request at a time and is answering another": "vyřizuje jeden požadavek najednou a právě vyřizuje jiný",
@@ -475,6 +491,7 @@ const LEMBAS_I18N = {
"late": "se zpožděním",
"light": "světlý",
"messages: %(n)s": "zprávy: %(n)s",
"mount": "připojení",
"not linked": "nepropojeno",
"not linked now": "teď není propojený",
"off": "vypnuto",
@@ -534,6 +551,7 @@ const LEMBAS_I18N = {
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Eine Aufgabe ohne eigenes Modell läuft auf dem ersten freien davon, in dieser Reihenfolge. Keines angehakt: alle.",
"About": "Über",
"Account": "Konto",
"Actions": "Aktionen",
"Activity": "Aktivität",
"Add": "Hinzufügen",
"Add folder…": "Ordner hinzufügen…",
@@ -541,6 +559,7 @@ const LEMBAS_I18N = {
"Agent": "Agent",
"Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Agenten-Chats können in diesen Ordnern und allem darin arbeiten — nirgendwo sonst auf diesem Computer.",
"Agent: %(why)s": "Agent: %(why)s",
"All files": "Alle Dateien",
"Allow": "Erlauben",
"Always": "Immer",
"Always allow": "Immer erlauben",
@@ -564,10 +583,12 @@ const LEMBAS_I18N = {
"Background tasks": "Hintergrundaufgaben",
"Cancel": "Abbrechen",
"Carry it out": "Ausführen",
"Changed": "Geändert",
"Chat": "Chat",
"Chats": "Chats",
"Check for updates…": "Nach Updates suchen…",
"Choose": "Auswählen",
"Choose a file to see it here.": "Wählen Sie eine Datei, um sie hier zu sehen.",
"Choose a folder first.": "Wählen Sie zuerst einen Ordner.",
"Choose a folder…": "Ordner wählen…",
"Choose a model to choose its effort.": "Wählen Sie ein Modell, um seinen Aufwand zu wählen.",
@@ -588,6 +609,7 @@ const LEMBAS_I18N = {
"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…",
"Contents": "Inhalt",
"Context": "Kontext",
"Context and background tasks": "Kontext und Hintergrundaufgaben",
"Context used": "Genutzter Kontext",
@@ -609,6 +631,7 @@ const LEMBAS_I18N = {
"Delete this chat?": "Diesen Chat löschen?",
"Delete this scheduled task?": "Diese geplante Aufgabe löschen?",
"Deny": "Ablehnen",
"Details": "Details",
"Dictate a message": "Nachricht diktieren",
"Disconnected. Close and reopen to reconnect.": "Getrennt. Schließen und erneut öffnen, um neu zu verbinden.",
"Done": "Fertig",
@@ -642,10 +665,12 @@ const LEMBAS_I18N = {
"Folder name": "Ordnername",
"Folder on %(device)s": "Ordner auf %(device)s",
"Folder on the computer": "Ordner auf dem Computer",
"Folders": "Ordner",
"Free space": "Freier Platz",
"Hello": "Hallo",
"Hello, %(name)s": "Hallo, %(name)s",
"History": "Verlauf",
"Home": "Start",
"How many work on it at once, the run's own agent among them, on the same model.": "Wie viele gleichzeitig daran arbeiten, der eigene Agent des Laufs mitgezählt, mit demselben Modell.",
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "In Ihren eigenen Worten. Der Server macht daraus eine Regel und eine Anweisung und zeigt Ihnen die nächsten Läufe, bevor etwas gespeichert wird.",
"Install LLeMbas %(version)s…": "LLeMbas %(version)s installieren…",
@@ -684,10 +709,12 @@ const LEMBAS_I18N = {
"Models": "Modelle",
"Models for scheduled tasks": "Modelle für geplante Aufgaben",
"More": "Mehr",
"More for %(name)s": "Mehr für %(name)s",
"Move to folder…": "In Ordner verschieben…",
"Move to the trash": "In den Papierkorb",
"Move “%(name)s” to the trash? It can be restored from there.": "„%(name)s“ in den Papierkorb verschieben? Von dort lässt es sich wiederherstellen.",
"Moved to the trash.": "In den Papierkorb verschoben.",
"Name": "Name",
"Never": "Nie",
"New agent chat": "Neuer Agenten-Chat",
"New chat": "Neuer Chat",
@@ -712,6 +739,7 @@ const LEMBAS_I18N = {
"Noted by a model": "Von einem Modell notiert",
"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 here yet. Not even a hobbit hole.": "Hier ist noch nichts. Nicht einmal eine Hobbithöhle.",
"Nothing is scheduled.": "Nichts ist geplant.",
"Nothing matches.": "Nichts gefunden.",
"Nothing remembered yet.": "Noch nichts behalten.",
@@ -737,6 +765,7 @@ const LEMBAS_I18N = {
"Or type an answer": "Oder eine Antwort eingeben",
"Outputs": "Ausgaben",
"Page": "Seite",
"Path": "Pfad",
"Pause": "Pausieren",
"Pause %(title)s": "%(title)s pausieren",
"Paused": "Pausiert",
@@ -803,6 +832,7 @@ const LEMBAS_I18N = {
"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.",
"Since the last reply": "Seit der letzten Antwort",
"Size": "Größe",
"Skills": "Fertigkeiten",
"Skipped": "Ausgelassen",
"Something to remember…": "Etwas, das behalten werden soll…",
@@ -899,6 +929,7 @@ const LEMBAS_I18N = {
"Tools": "Werkzeuge",
"Tools and skills…": "Werkzeuge und Fähigkeiten…",
"Try again": "Erneut versuchen",
"Type": "Typ",
"Type a command or a chat's title": "Befehl oder Chat-Titel eingeben",
"Type a folder…": "Ordner eingeben…",
"Unpin chat": "Chat loslösen",
@@ -920,6 +951,7 @@ const LEMBAS_I18N = {
"When": "Wann",
"When it matters": "Wenn es zählt",
"Where it runs": "Wo es läuft",
"Where you are": "Wo Sie sind",
"Wide": "Breit",
"Writing": "Schreibe",
"Writing down…": "Wird aufgeschrieben…",
@@ -955,6 +987,7 @@ const LEMBAS_I18N = {
"failed": "fehlgeschlagen",
"findings: %(n)s": "Erkenntnisse: %(n)s",
"finished": "fertig",
"folder": "Ordner",
"free": "frei",
"idle": "untätig",
"it serves one request at a time and is answering another": "es bedient eine Anfrage auf einmal und beantwortet gerade eine andere",
@@ -962,6 +995,7 @@ const LEMBAS_I18N = {
"late": "verspätet",
"light": "hell",
"messages: %(n)s": "Nachrichten: %(n)s",
"mount": "Einhängung",
"not linked": "nicht verbunden",
"not linked now": "gerade nicht verbunden",
"off": "aus",
@@ -1021,6 +1055,7 @@ const LEMBAS_I18N = {
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Una tarea sin modelo propio se ejecuta en el primero libre de estos, en este orden. Ninguno marcado: todos.",
"About": "Acerca de",
"Account": "Cuenta",
"Actions": "Acciones",
"Activity": "Actividad",
"Add": "Añadir",
"Add folder…": "Añadir carpeta…",
@@ -1028,6 +1063,7 @@ const LEMBAS_I18N = {
"Agent": "Agente",
"Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Los chats de agente pueden trabajar en estas carpetas y en todo lo que contienen — en ningún otro lugar de este equipo.",
"Agent: %(why)s": "Agente: %(why)s",
"All files": "Todos los archivos",
"Allow": "Permitir",
"Always": "Siempre",
"Always allow": "Permitir siempre",
@@ -1051,10 +1087,12 @@ const LEMBAS_I18N = {
"Background tasks": "Tareas en segundo plano",
"Cancel": "Cancelar",
"Carry it out": "Llevarlo a cabo",
"Changed": "Cambiado",
"Chat": "Chat",
"Chats": "Chats",
"Check for updates…": "Buscar actualizaciones…",
"Choose": "Elegir",
"Choose a file to see it here.": "Elige un archivo para verlo aquí.",
"Choose a folder first.": "Elija primero una carpeta.",
"Choose a folder…": "Elegir una carpeta…",
"Choose a model to choose its effort.": "Elija un modelo para elegir su esfuerzo.",
@@ -1075,6 +1113,7 @@ const LEMBAS_I18N = {
"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…",
"Contents": "Contenido",
"Context": "Contexto",
"Context and background tasks": "Contexto y tareas en segundo plano",
"Context used": "Contexto usado",
@@ -1096,6 +1135,7 @@ const LEMBAS_I18N = {
"Delete this chat?": "¿Eliminar este chat?",
"Delete this scheduled task?": "¿Eliminar esta tarea programada?",
"Deny": "Denegar",
"Details": "Detalles",
"Dictate a message": "Dictar un mensaje",
"Disconnected. Close and reopen to reconnect.": "Desconectado. Cierre y vuelva a abrir para reconectar.",
"Done": "Hecho",
@@ -1129,10 +1169,12 @@ const LEMBAS_I18N = {
"Folder name": "Nombre de la carpeta",
"Folder on %(device)s": "Carpeta en %(device)s",
"Folder on the computer": "Carpeta en el ordenador",
"Folders": "Carpetas",
"Free space": "Espacio libre",
"Hello": "Hola",
"Hello, %(name)s": "Hola, %(name)s",
"History": "Historial",
"Home": "Inicio",
"How many work on it at once, the run's own agent among them, on the same model.": "Cuántos trabajan en ella a la vez, contando el propio agente de la ejecución, con el mismo modelo.",
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Con sus propias palabras. El servidor las convierte en una regla y una instrucción, y le muestra las próximas ejecuciones antes de guardar nada.",
"Install LLeMbas %(version)s…": "Instalar LLeMbas %(version)s…",
@@ -1171,10 +1213,12 @@ const LEMBAS_I18N = {
"Models": "Modelos",
"Models for scheduled tasks": "Modelos para tareas programadas",
"More": "Más",
"More for %(name)s": "Más para %(name)s",
"Move to folder…": "Mover a una carpeta…",
"Move to the trash": "Mover a la papelera",
"Move “%(name)s” to the trash? It can be restored from there.": "¿Mover «%(name)s» a la papelera? Se puede restaurar desde allí.",
"Moved to the trash.": "Movido a la papelera.",
"Name": "Nombre",
"Never": "Nunca",
"New agent chat": "Nuevo chat de agente",
"New chat": "Nuevo chat",
@@ -1199,6 +1243,7 @@ const LEMBAS_I18N = {
"Noted by a model": "Anotado por un modelo",
"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 here yet. Not even a hobbit hole.": "Aquí aún no hay nada. Ni siquiera un agujero hobbit.",
"Nothing is scheduled.": "No hay nada programado.",
"Nothing matches.": "No hay coincidencias.",
"Nothing remembered yet.": "Todavía no se recuerda nada.",
@@ -1224,6 +1269,7 @@ const LEMBAS_I18N = {
"Or type an answer": "O escriba una respuesta",
"Outputs": "Resultados",
"Page": "Página",
"Path": "Ruta",
"Pause": "Pausar",
"Pause %(title)s": "Pausar %(title)s",
"Paused": "En pausa",
@@ -1290,6 +1336,7 @@ const LEMBAS_I18N = {
"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.",
"Since the last reply": "Desde la última respuesta",
"Size": "Tamaño",
"Skills": "Habilidades",
"Skipped": "Omitida",
"Something to remember…": "Algo que recordar…",
@@ -1386,6 +1433,7 @@ const LEMBAS_I18N = {
"Tools": "Herramientas",
"Tools and skills…": "Herramientas y habilidades…",
"Try again": "Reintentar",
"Type": "Tipo",
"Type a command or a chat's title": "Escriba un comando o el título de un chat",
"Type a folder…": "Escribir una carpeta…",
"Unpin chat": "Dejar de fijar el chat",
@@ -1407,6 +1455,7 @@ const LEMBAS_I18N = {
"When": "Cuándo",
"When it matters": "Cuando importa",
"Where it runs": "Dónde se ejecuta",
"Where you are": "Dónde está",
"Wide": "Ancha",
"Writing": "Escribiendo",
"Writing down…": "Transcribiendo…",
@@ -1442,6 +1491,7 @@ const LEMBAS_I18N = {
"failed": "falló",
"findings: %(n)s": "hallazgos: %(n)s",
"finished": "terminado",
"folder": "carpeta",
"free": "libre",
"idle": "inactivo",
"it serves one request at a time and is answering another": "atiende una petición a la vez y está respondiendo otra",
@@ -1449,6 +1499,7 @@ const LEMBAS_I18N = {
"late": "con retraso",
"light": "claro",
"messages: %(n)s": "mensajes: %(n)s",
"mount": "montaje",
"not linked": "no vinculado",
"not linked now": "no vinculado ahora",
"off": "desactivado",
@@ -1508,6 +1559,7 @@ const LEMBAS_I18N = {
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Une tâche sans modèle à elle tourne sur le premier libre de ceux-ci, dans cet ordre. Aucun coché : tous.",
"About": "À propos",
"Account": "Compte",
"Actions": "Actions",
"Activity": "Activité",
"Add": "Ajouter",
"Add folder…": "Ajouter un dossier…",
@@ -1515,6 +1567,7 @@ const LEMBAS_I18N = {
"Agent": "Agent",
"Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Les discussions d'agent peuvent travailler dans ces dossiers et tout ce qu'ils contiennent — nulle part ailleurs sur cet ordinateur.",
"Agent: %(why)s": "Agent : %(why)s",
"All files": "Tous les fichiers",
"Allow": "Autoriser",
"Always": "Toujours",
"Always allow": "Toujours autoriser",
@@ -1538,10 +1591,12 @@ const LEMBAS_I18N = {
"Background tasks": "Tâches en arrière-plan",
"Cancel": "Annuler",
"Carry it out": "Exécuter",
"Changed": "Modifié",
"Chat": "Conversation",
"Chats": "Conversations",
"Check for updates…": "Rechercher des mises à jour…",
"Choose": "Choisir",
"Choose a file to see it here.": "Choisissez un fichier pour le voir ici.",
"Choose a folder first.": "Choisissez d’abord un dossier.",
"Choose a folder…": "Choisir un dossier…",
"Choose a model to choose its effort.": "Choisissez un modèle pour choisir son effort.",
@@ -1562,6 +1617,7 @@ const LEMBAS_I18N = {
"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…",
"Contents": "Contenu",
"Context": "Contexte",
"Context and background tasks": "Contexte et tâches en arrière-plan",
"Context used": "Contexte utilisé",
@@ -1583,6 +1639,7 @@ const LEMBAS_I18N = {
"Delete this chat?": "Supprimer cette conversation ?",
"Delete this scheduled task?": "Supprimer cette tâche planifiée ?",
"Deny": "Refuser",
"Details": "Détails",
"Dictate a message": "Dicter un message",
"Disconnected. Close and reopen to reconnect.": "Déconnecté. Fermez et rouvrez pour vous reconnecter.",
"Done": "Terminé",
@@ -1616,10 +1673,12 @@ const LEMBAS_I18N = {
"Folder name": "Nom du dossier",
"Folder on %(device)s": "Dossier sur %(device)s",
"Folder on the computer": "Dossier sur l’ordinateur",
"Folders": "Dossiers",
"Free space": "Espace libre",
"Hello": "Bonjour",
"Hello, %(name)s": "Bonjour, %(name)s",
"History": "Historique",
"Home": "Accueil",
"How many work on it at once, the run's own agent among them, on the same model.": "Combien y travaillent à la fois, l’agent de l’exécution compris, avec le même modèle.",
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Avec vos propres mots. Le serveur en fait une règle et une instruction, et vous montre les prochaines exécutions avant que rien ne soit enregistré.",
"Install LLeMbas %(version)s…": "Installer LLeMbas %(version)s…",
@@ -1658,10 +1717,12 @@ const LEMBAS_I18N = {
"Models": "Modèles",
"Models for scheduled tasks": "Modèles pour les tâches planifiées",
"More": "Plus",
"More for %(name)s": "Plus pour %(name)s",
"Move to folder…": "Déplacer dans un dossier…",
"Move to the trash": "Placer dans la corbeille",
"Move “%(name)s” to the trash? It can be restored from there.": "Placer « %(name)s » dans la corbeille ? On peut l'y restaurer.",
"Moved to the trash.": "Placé dans la corbeille.",
"Name": "Nom",
"Never": "Jamais",
"New agent chat": "Nouvelle discussion d'agent",
"New chat": "Nouvelle conversation",
@@ -1686,6 +1747,7 @@ const LEMBAS_I18N = {
"Noted by a model": "Noté par un modèle",
"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 here yet. Not even a hobbit hole.": "Rien ici pour l'instant. Pas même un trou de hobbit.",
"Nothing is scheduled.": "Rien n’est planifié.",
"Nothing matches.": "Aucun résultat.",
"Nothing remembered yet.": "Rien n’est encore retenu.",
@@ -1711,6 +1773,7 @@ const LEMBAS_I18N = {
"Or type an answer": "Ou saisissez une réponse",
"Outputs": "Sorties",
"Page": "Page",
"Path": "Chemin",
"Pause": "Mettre en pause",
"Pause %(title)s": "Mettre %(title)s en pause",
"Paused": "En pause",
@@ -1777,6 +1840,7 @@ const LEMBAS_I18N = {
"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.",
"Since the last reply": "Depuis la dernière réponse",
"Size": "Taille",
"Skills": "Compétences",
"Skipped": "Ignorée",
"Something to remember…": "Quelque chose à retenir…",
@@ -1873,6 +1937,7 @@ const LEMBAS_I18N = {
"Tools": "Outils",
"Tools and skills…": "Outils et compétences…",
"Try again": "Réessayer",
"Type": "Type",
"Type a command or a chat's title": "Tapez une commande ou le titre d'une conversation",
"Type a folder…": "Saisir un dossier…",
"Unpin chat": "Désépingler la discussion",
@@ -1894,6 +1959,7 @@ const LEMBAS_I18N = {
"When": "Quand",
"When it matters": "Quand c'est important",
"Where it runs": "Où elle s’exécute",
"Where you are": "Où vous êtes",
"Wide": "Large",
"Writing": "Écriture",
"Writing down…": "Transcription…",
@@ -1929,6 +1995,7 @@ const LEMBAS_I18N = {
"failed": "échoué",
"findings: %(n)s": "constats : %(n)s",
"finished": "terminé",
"folder": "dossier",
"free": "libre",
"idle": "inactif",
"it serves one request at a time and is answering another": "il traite une requête à la fois et en traite une autre",
@@ -1936,6 +2003,7 @@ const LEMBAS_I18N = {
"late": "en retard",
"light": "clair",
"messages: %(n)s": "messages : %(n)s",
"mount": "montage",
"not linked": "non relié",
"not linked now": "non relié pour l’instant",
"off": "désactivé",
@@ -1995,6 +2063,7 @@ const LEMBAS_I18N = {
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "A saját modell nélküli feladat ezek közül az első szabadon fut, ebben a sorrendben. Ha egy sincs bejelölve: mindegyiken.",
"About": "Névjegy",
"Account": "Fiók",
"Actions": "Műveletek",
"Activity": "Tevékenység",
"Add": "Hozzáadás",
"Add folder…": "Mappa hozzáadása…",
@@ -2002,6 +2071,7 @@ const LEMBAS_I18N = {
"Agent": "Ügynök",
"Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Az ügynökcsevegések ezekben a mappákban és mindenben, ami bennük van, dolgozhatnak — sehol máshol ezen a számítógépen.",
"Agent: %(why)s": "Ügynök: %(why)s",
"All files": "Minden fájl",
"Allow": "Engedélyezés",
"Always": "Mindig",
"Always allow": "Mindig engedélyez",
@@ -2025,10 +2095,12 @@ const LEMBAS_I18N = {
"Background tasks": "Háttérfeladatok",
"Cancel": "Mégse",
"Carry it out": "Végrehajtás",
"Changed": "Módosítva",
"Chat": "Csevegés",
"Chats": "Csevegések",
"Check for updates…": "Frissítések keresése…",
"Choose": "Kiválasztás",
"Choose a file to see it here.": "Válasszon egy fájlt, hogy itt lássa.",
"Choose a folder first.": "Először válasszon mappát.",
"Choose a folder…": "Mappa kiválasztása…",
"Choose a model to choose its effort.": "Válasszon modellt az erőfeszítés megadásához.",
@@ -2049,6 +2121,7 @@ const LEMBAS_I18N = {
"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…",
"Contents": "Tartalom",
"Context": "Kontextus",
"Context and background tasks": "Kontextus és háttérfeladatok",
"Context used": "Felhasznált kontextus",
@@ -2070,6 +2143,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",
"Details": "Részletek",
"Dictate a message": "Üzenet diktálása",
"Disconnected. Close and reopen to reconnect.": "Kapcsolat bontva. Az újrakapcsolódáshoz zárja be és nyissa meg újra.",
"Done": "Kész",
@@ -2103,10 +2177,12 @@ const LEMBAS_I18N = {
"Folder name": "Mappa neve",
"Folder on %(device)s": "Mappa ezen: %(device)s",
"Folder on the computer": "Mappa a számítógépen",
"Folders": "Mappák",
"Free space": "Szabad hely",
"Hello": "Üdv",
"Hello, %(name)s": "Üdv, %(name)s",
"History": "Előzmények",
"Home": "Kezdőlap",
"How many work on it at once, the run's own agent among them, on the same model.": "Hányan dolgoznak rajta egyszerre, a futás saját ügynökét is beleértve, ugyanazon a modellen.",
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "A saját szavaival. A szerver szabályt és utasítást készít belőlük, és mentés előtt megmutatja a következő futásokat.",
"Install LLeMbas %(version)s…": "LLeMbas %(version)s telepítése…",
@@ -2145,10 +2221,12 @@ const LEMBAS_I18N = {
"Models": "Modellek",
"Models for scheduled tasks": "Modellek az ütemezett feladatokhoz",
"More": "Több",
"More for %(name)s": "Továbbiak: %(name)s",
"Move to folder…": "Áthelyezés mappába…",
"Move to the trash": "Áthelyezés a lomtárba",
"Move “%(name)s” to the trash? It can be restored from there.": "Áthelyezi a lomtárba: „%(name)s”? Onnan visszaállítható.",
"Moved to the trash.": "A lomtárba került.",
"Name": "Név",
"Never": "Soha",
"New agent chat": "Új ügynökcsevegés",
"New chat": "Új csevegés",
@@ -2173,6 +2251,7 @@ const LEMBAS_I18N = {
"Noted by a model": "Egy modell jegyezte fel",
"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 here yet. Not even a hobbit hole.": "Itt még nincs semmi. Még egy hobbitlyuk sem.",
"Nothing is scheduled.": "Nincs semmi ütemezve.",
"Nothing matches.": "Nincs találat.",
"Nothing remembered yet.": "Még nincs megjegyezve semmi.",
@@ -2198,6 +2277,7 @@ const LEMBAS_I18N = {
"Or type an answer": "Vagy írjon választ",
"Outputs": "Kimenetek",
"Page": "Oldal",
"Path": "Útvonal",
"Pause": "Szüneteltetés",
"Pause %(title)s": "%(title)s szüneteltetése",
"Paused": "Szüneteltetve",
@@ -2264,6 +2344,7 @@ const LEMBAS_I18N = {
"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.",
"Since the last reply": "Az utolsó válasz óta",
"Size": "Méret",
"Skills": "Készségek",
"Skipped": "Kihagyva",
"Something to remember…": "Valami, amit meg kell jegyezni…",
@@ -2360,6 +2441,7 @@ const LEMBAS_I18N = {
"Tools": "Eszközök",
"Tools and skills…": "Eszközök és készségek…",
"Try again": "Újrapróbálás",
"Type": "Típus",
"Type a command or a chat's title": "Írjon be egy parancsot vagy egy csevegés címét",
"Type a folder…": "Mappa megadása…",
"Unpin chat": "Kitűzés megszüntetése",
@@ -2381,6 +2463,7 @@ const LEMBAS_I18N = {
"When": "Mikor",
"When it matters": "Ha számít",
"Where it runs": "Hol fut",
"Where you are": "Hol van",
"Wide": "Széles",
"Writing": "Írás",
"Writing down…": "Leírás…",
@@ -2416,6 +2499,7 @@ const LEMBAS_I18N = {
"failed": "sikertelen",
"findings: %(n)s": "megállapítások: %(n)s",
"finished": "kész",
"folder": "mappa",
"free": "szabad",
"idle": "tétlen",
"it serves one request at a time and is answering another": "egyszerre egy kérést szolgál ki, és éppen egy másikat válaszol meg",
@@ -2423,6 +2507,7 @@ const LEMBAS_I18N = {
"late": "késve",
"light": "világos",
"messages: %(n)s": "üzenetek: %(n)s",
"mount": "csatolás",
"not linked": "nincs összekapcsolva",
"not linked now": "most nincs összekapcsolva",
"off": "ki",
@@ -2482,6 +2567,7 @@ const LEMBAS_I18N = {
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Un’attività senza un modello proprio gira sul primo libero di questi, in quest’ordine. Nessuno spuntato: tutti.",
"About": "Informazioni",
"Account": "Account",
"Actions": "Azioni",
"Activity": "Attività",
"Add": "Aggiungi",
"Add folder…": "Aggiungi cartella…",
@@ -2489,6 +2575,7 @@ const LEMBAS_I18N = {
"Agent": "Agente",
"Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Le chat con agente possono lavorare in queste cartelle e in tutto ciò che contengono — in nessun altro punto di questo computer.",
"Agent: %(why)s": "Agente: %(why)s",
"All files": "Tutti i file",
"Allow": "Consenti",
"Always": "Sempre",
"Always allow": "Consenti sempre",
@@ -2512,10 +2599,12 @@ const LEMBAS_I18N = {
"Background tasks": "Attività in background",
"Cancel": "Annulla",
"Carry it out": "Eseguilo",
"Changed": "Modificato",
"Chat": "Chat",
"Chats": "Chat",
"Check for updates…": "Cerca aggiornamenti…",
"Choose": "Scegli",
"Choose a file to see it here.": "Scegli un file per vederlo qui.",
"Choose a folder first.": "Scegli prima una cartella.",
"Choose a folder…": "Scegli una cartella…",
"Choose a model to choose its effort.": "Scegli un modello per sceglierne l’impegno.",
@@ -2536,6 +2625,7 @@ const LEMBAS_I18N = {
"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…",
"Contents": "Contenuto",
"Context": "Contesto",
"Context and background tasks": "Contesto e attività in background",
"Context used": "Contesto usato",
@@ -2557,6 +2647,7 @@ const LEMBAS_I18N = {
"Delete this chat?": "Eliminare questa chat?",
"Delete this scheduled task?": "Eliminare questa attività pianificata?",
"Deny": "Nega",
"Details": "Dettagli",
"Dictate a message": "Detta un messaggio",
"Disconnected. Close and reopen to reconnect.": "Disconnesso. Chiudi e riapri per riconnetterti.",
"Done": "Fatto",
@@ -2590,10 +2681,12 @@ const LEMBAS_I18N = {
"Folder name": "Nome della cartella",
"Folder on %(device)s": "Cartella su %(device)s",
"Folder on the computer": "Cartella sul computer",
"Folders": "Cartelle",
"Free space": "Spazio libero",
"Hello": "Ciao",
"Hello, %(name)s": "Ciao, %(name)s",
"History": "Cronologia",
"Home": "Home",
"How many work on it at once, the run's own agent among them, on the same model.": "Quanti ci lavorano alla volta, compreso l’agente dell’esecuzione, sullo stesso modello.",
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Con parole tue. Il server ne ricava una regola e un’istruzione e ti mostra le prossime esecuzioni prima di salvare.",
"Install LLeMbas %(version)s…": "Installa LLeMbas %(version)s…",
@@ -2632,10 +2725,12 @@ const LEMBAS_I18N = {
"Models": "Modelli",
"Models for scheduled tasks": "Modelli per le attività pianificate",
"More": "Altro",
"More for %(name)s": "Altro per %(name)s",
"Move to folder…": "Sposta in una cartella…",
"Move to the trash": "Sposta nel cestino",
"Move “%(name)s” to the trash? It can be restored from there.": "Spostare «%(name)s» nel cestino? Si può ripristinare da lì.",
"Moved to the trash.": "Spostato nel cestino.",
"Name": "Nome",
"Never": "Mai",
"New agent chat": "Nuova chat con agente",
"New chat": "Nuova chat",
@@ -2660,6 +2755,7 @@ const LEMBAS_I18N = {
"Noted by a model": "Annotato da un modello",
"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 here yet. Not even a hobbit hole.": "Ancora niente qui. Nemmeno una caverna hobbit.",
"Nothing is scheduled.": "Non c’è niente di pianificato.",
"Nothing matches.": "Nessun risultato.",
"Nothing remembered yet.": "Ancora niente da ricordare.",
@@ -2685,6 +2781,7 @@ const LEMBAS_I18N = {
"Or type an answer": "Oppure scriva una risposta",
"Outputs": "Risultati",
"Page": "Pagina",
"Path": "Percorso",
"Pause": "Metti in pausa",
"Pause %(title)s": "Metti in pausa %(title)s",
"Paused": "In pausa",
@@ -2751,6 +2848,7 @@ const LEMBAS_I18N = {
"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.",
"Since the last reply": "Dall’ultima risposta",
"Size": "Dimensione",
"Skills": "Abilità",
"Skipped": "Saltata",
"Something to remember…": "Qualcosa da ricordare…",
@@ -2847,6 +2945,7 @@ const LEMBAS_I18N = {
"Tools": "Strumenti",
"Tools and skills…": "Strumenti e abilità…",
"Try again": "Riprova",
"Type": "Tipo",
"Type a command or a chat's title": "Digiti un comando o il titolo di una chat",
"Type a folder…": "Digita una cartella…",
"Unpin chat": "Togli la chat dalle fissate",
@@ -2868,6 +2967,7 @@ const LEMBAS_I18N = {
"When": "Quando",
"When it matters": "Quando conta",
"Where it runs": "Dove viene eseguito",
"Where you are": "Dove sei",
"Wide": "Larga",
"Writing": "Scrittura",
"Writing down…": "Trascrizione…",
@@ -2903,6 +3003,7 @@ const LEMBAS_I18N = {
"failed": "fallito",
"findings: %(n)s": "risultati: %(n)s",
"finished": "finito",
"folder": "cartella",
"free": "libero",
"idle": "inattivo",
"it serves one request at a time and is answering another": "serve una richiesta alla volta e ne sta servendo un’altra",
@@ -2910,6 +3011,7 @@ const LEMBAS_I18N = {
"late": "in ritardo",
"light": "chiaro",
"messages: %(n)s": "messaggi: %(n)s",
"mount": "montaggio",
"not linked": "non collegato",
"not linked now": "non collegato ora",
"off": "disattivato",
@@ -2969,6 +3071,7 @@ const LEMBAS_I18N = {
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Zadanie bez własnego modelu działa na pierwszym wolnym z tych, w tej kolejności. Żaden niezaznaczony: wszystkie.",
"About": "O aplikacji",
"Account": "Konto",
"Actions": "Działania",
"Activity": "Aktywność",
"Add": "Dodaj",
"Add folder…": "Dodaj folder…",
@@ -2976,6 +3079,7 @@ const LEMBAS_I18N = {
"Agent": "Agent",
"Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Czaty z agentem mogą pracować w tych folderach i we wszystkim, co zawierają — nigdzie indziej na tym komputerze.",
"Agent: %(why)s": "Agent: %(why)s",
"All files": "Wszystkie pliki",
"Allow": "Pozwól",
"Always": "Zawsze",
"Always allow": "Zawsze zezwalaj",
@@ -2999,10 +3103,12 @@ const LEMBAS_I18N = {
"Background tasks": "Zadania w tle",
"Cancel": "Anuluj",
"Carry it out": "Wykonaj",
"Changed": "Zmieniono",
"Chat": "Czat",
"Chats": "Czaty",
"Check for updates…": "Sprawdź aktualizacje…",
"Choose": "Wybierz",
"Choose a file to see it here.": "Wybierz plik, aby zobaczyć go tutaj.",
"Choose a folder first.": "Najpierw wybierz folder.",
"Choose a folder…": "Wybierz folder…",
"Choose a model to choose its effort.": "Wybierz model, aby wybrać jego wysiłek.",
@@ -3023,6 +3129,7 @@ const LEMBAS_I18N = {
"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…",
"Contents": "Zawartość",
"Context": "Kontekst",
"Context and background tasks": "Kontekst i zadania w tle",
"Context used": "Użyty kontekst",
@@ -3044,6 +3151,7 @@ const LEMBAS_I18N = {
"Delete this chat?": "Usunąć ten czat?",
"Delete this scheduled task?": "Usunąć to zaplanowane zadanie?",
"Deny": "Odrzuć",
"Details": "Szczegóły",
"Dictate a message": "Podyktuj wiadomość",
"Disconnected. Close and reopen to reconnect.": "Rozłączono. Aby połączyć ponownie, zamknij i otwórz ponownie.",
"Done": "Gotowe",
@@ -3077,10 +3185,12 @@ const LEMBAS_I18N = {
"Folder name": "Nazwa folderu",
"Folder on %(device)s": "Folder na %(device)s",
"Folder on the computer": "Folder na komputerze",
"Folders": "Foldery",
"Free space": "Wolne miejsce",
"Hello": "Cześć",
"Hello, %(name)s": "Cześć, %(name)s",
"History": "Historia",
"Home": "Start",
"How many work on it at once, the run's own agent among them, on the same model.": "Ilu pracuje nad nim naraz, łącznie z własnym agentem uruchomienia, na tym samym modelu.",
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Własnymi słowami. Serwer zamieni je w regułę i polecenie i pokaże następne uruchomienia, zanim cokolwiek zostanie zapisane.",
"Install LLeMbas %(version)s…": "Zainstaluj LLeMbas %(version)s…",
@@ -3119,10 +3229,12 @@ const LEMBAS_I18N = {
"Models": "Modele",
"Models for scheduled tasks": "Modele dla zaplanowanych zadań",
"More": "Więcej",
"More for %(name)s": "Więcej dla %(name)s",
"Move to folder…": "Przenieś do folderu…",
"Move to the trash": "Przenieś do kosza",
"Move “%(name)s” to the trash? It can be restored from there.": "Przenieść „%(name)s” do kosza? Można go stamtąd przywrócić.",
"Moved to the trash.": "Przeniesiono do kosza.",
"Name": "Nazwa",
"Never": "Nigdy",
"New agent chat": "Nowy czat z agentem",
"New chat": "Nowy czat",
@@ -3147,6 +3259,7 @@ const LEMBAS_I18N = {
"Noted by a model": "Zanotowane przez model",
"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 here yet. Not even a hobbit hole.": "Jeszcze nic tu nie ma. Nawet hobbiciej norki.",
"Nothing is scheduled.": "Nic nie jest zaplanowane.",
"Nothing matches.": "Nic nie pasuje.",
"Nothing remembered yet.": "Na razie nic nie zapamiętano.",
@@ -3172,6 +3285,7 @@ const LEMBAS_I18N = {
"Or type an answer": "Albo wpisz odpowiedź",
"Outputs": "Wyniki",
"Page": "Strona",
"Path": "Ścieżka",
"Pause": "Wstrzymaj",
"Pause %(title)s": "Wstrzymaj %(title)s",
"Paused": "Wstrzymany",
@@ -3238,6 +3352,7 @@ const LEMBAS_I18N = {
"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.",
"Since the last reply": "Od ostatniej odpowiedzi",
"Size": "Rozmiar",
"Skills": "Umiejętności",
"Skipped": "Pominięty",
"Something to remember…": "Coś do zapamiętania…",
@@ -3334,6 +3449,7 @@ const LEMBAS_I18N = {
"Tools": "Narzędzia",
"Tools and skills…": "Narzędzia i umiejętności…",
"Try again": "Spróbuj ponownie",
"Type": "Typ",
"Type a command or a chat's title": "Wpisz polecenie lub tytuł czatu",
"Type a folder…": "Wpisz folder…",
"Unpin chat": "Odepnij czat",
@@ -3355,6 +3471,7 @@ const LEMBAS_I18N = {
"When": "Kiedy",
"When it matters": "Gdy to ważne",
"Where it runs": "Gdzie działa",
"Where you are": "Gdzie jesteś",
"Wide": "Szeroki",
"Writing": "Pisanie",
"Writing down…": "Zapisywanie…",
@@ -3390,6 +3507,7 @@ const LEMBAS_I18N = {
"failed": "nie powiodło się",
"findings: %(n)s": "ustalenia: %(n)s",
"finished": "zakończony",
"folder": "folder",
"free": "wolny",
"idle": "bezczynny",
"it serves one request at a time and is answering another": "obsługuje jedno żądanie naraz i właśnie obsługuje inne",
@@ -3397,6 +3515,7 @@ const LEMBAS_I18N = {
"late": "z opóźnieniem",
"light": "jasny",
"messages: %(n)s": "wiadomości: %(n)s",
"mount": "montowanie",
"not linked": "niepołączony",
"not linked now": "teraz niepołączony",
"off": "wył.",
@@ -3456,6 +3575,7 @@ const LEMBAS_I18N = {
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Úloha bez vlastného modelu beží na prvom voľnom z týchto, v tomto poradí. Žiadny nezaškrtnutý: všetky.",
"About": "O aplikácii",
"Account": "Účet",
"Actions": "Akcie",
"Activity": "Aktivita",
"Add": "Pridať",
"Add folder…": "Pridať priečinok…",
@@ -3463,6 +3583,7 @@ const LEMBAS_I18N = {
"Agent": "Agent",
"Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Agentské konverzácie môžu pracovať v týchto priečinkoch a vo všetkom, čo obsahujú — nikde inde na tomto počítači.",
"Agent: %(why)s": "Agent: %(why)s",
"All files": "Všetky súbory",
"Allow": "Povoliť",
"Always": "Vždy",
"Always allow": "Vždy povoliť",
@@ -3486,10 +3607,12 @@ const LEMBAS_I18N = {
"Background tasks": "Úlohy na pozadí",
"Cancel": "Zrušiť",
"Carry it out": "Vykonať to",
"Changed": "Zmenené",
"Chat": "Konverzácia",
"Chats": "Konverzácie",
"Check for updates…": "Skontrolovať aktualizácie…",
"Choose": "Vybrať",
"Choose a file to see it here.": "Vyberte súbor a uvidíte ho tu.",
"Choose a folder first.": "Najprv vyberte priečinok.",
"Choose a folder…": "Vybrať priečinok…",
"Choose a model to choose its effort.": "Vyberte model, aby ste mohli zvoliť jeho náročnosť.",
@@ -3510,6 +3633,7 @@ const LEMBAS_I18N = {
"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…",
"Contents": "Obsah",
"Context": "Kontext",
"Context and background tasks": "Kontext a úlohy na pozadí",
"Context used": "Využitý kontext",
@@ -3531,6 +3655,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ť",
"Details": "Podrobnosti",
"Dictate a message": "Nadiktovať správu",
"Disconnected. Close and reopen to reconnect.": "Odpojené. Zavrite a znova otvorte, aby sa pripojilo.",
"Done": "Hotovo",
@@ -3564,10 +3689,12 @@ const LEMBAS_I18N = {
"Folder name": "Názov zložky",
"Folder on %(device)s": "Priečinok na zariadení %(device)s",
"Folder on the computer": "Priečinok na počítači",
"Folders": "Priečinky",
"Free space": "Voľné miesto",
"Hello": "Dobrý deň",
"Hello, %(name)s": "Dobrý deň, %(name)s",
"History": "História",
"Home": "Domov",
"How many work on it at once, the run's own agent among them, on the same model.": "Koľko na nej pracuje naraz, vrátane vlastného agenta behu, na rovnakom modeli.",
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Vlastnými slovami. Server z nich vytvorí pravidlo a pokyn a pred uložením vám ukáže ďalšie spustenia.",
"Install LLeMbas %(version)s…": "Nainštalovať LLeMbas %(version)s…",
@@ -3606,10 +3733,12 @@ const LEMBAS_I18N = {
"Models": "Modely",
"Models for scheduled tasks": "Modely pre naplánované úlohy",
"More": "Viac",
"More for %(name)s": "Ďalšie pre %(name)s",
"Move to folder…": "Presunúť do priečinka…",
"Move to the trash": "Presunúť do koša",
"Move “%(name)s” to the trash? It can be restored from there.": "Presunúť „%(name)s“ do koša? Odtiaľ sa dá obnoviť.",
"Moved to the trash.": "Presunuté do koša.",
"Name": "Názov",
"Never": "Nikdy",
"New agent chat": "Nová agentská konverzácia",
"New chat": "Nová konverzácia",
@@ -3634,6 +3763,7 @@ const LEMBAS_I18N = {
"Noted by a model": "Zaznamenal model",
"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 here yet. Not even a hobbit hole.": "Zatiaľ tu nič nie je. Ani hobitia nora.",
"Nothing is scheduled.": "Nič nie je naplánované.",
"Nothing matches.": "Nič nezodpovedá.",
"Nothing remembered yet.": "Zatiaľ si nič nepamätá.",
@@ -3659,6 +3789,7 @@ const LEMBAS_I18N = {
"Or type an answer": "Alebo napíšte odpoveď",
"Outputs": "Výstupy",
"Page": "Stránka",
"Path": "Cesta",
"Pause": "Pozastaviť",
"Pause %(title)s": "Pozastaviť %(title)s",
"Paused": "Pozastavené",
@@ -3725,6 +3856,7 @@ const LEMBAS_I18N = {
"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.",
"Since the last reply": "Od poslednej odpovede",
"Size": "Veľkosť",
"Skills": "Schopnosti",
"Skipped": "Vynechané",
"Something to remember…": "Niečo na zapamätanie…",
@@ -3821,6 +3953,7 @@ const LEMBAS_I18N = {
"Tools": "Nástroje",
"Tools and skills…": "Nástroje a schopnosti…",
"Try again": "Skúsiť znova",
"Type": "Typ",
"Type a command or a chat's title": "Napíšte príkaz alebo názov konverzácie",
"Type a folder…": "Zadať priečinok…",
"Unpin chat": "Odopnúť konverzáciu",
@@ -3842,6 +3975,7 @@ const LEMBAS_I18N = {
"When": "Kedy",
"When it matters": "Keď na tom záleží",
"Where it runs": "Kde sa spúšťa",
"Where you are": "Kde ste",
"Wide": "Široký",
"Writing": "Píše",
"Writing down…": "Zapisujem…",
@@ -3877,6 +4011,7 @@ const LEMBAS_I18N = {
"failed": "zlyhalo",
"findings: %(n)s": "zistenia: %(n)s",
"finished": "dokončený",
"folder": "priečinok",
"free": "voľný",
"idle": "nečinný",
"it serves one request at a time and is answering another": "vybavuje jednu požiadavku naraz a práve vybavuje inú",
@@ -3884,6 +4019,7 @@ const LEMBAS_I18N = {
"late": "s oneskorením",
"light": "svetlá",
"messages: %(n)s": "správy: %(n)s",
"mount": "pripojenie",
"not linked": "neprepojené",
"not linked now": "teraz nie je prepojený",
"off": "vypnuté",
+27 -13
View File
@@ -100,22 +100,36 @@
<div class="head__tools"><div class="tabs" role="tablist" id="files-tabs"></div></div>
</header>
<div class="page__body files">
<section class="files__pane" id="pane-local" aria-label="This computer" data-i18n-aria-label>
<div class="files__head">
<div class="files__where" id="local-where"></div>
<button type="button" class="tool" id="local-refresh" aria-label="Read again" title="Read again" data-i18n-aria-label data-i18n-title></button>
<section class="fx" id="pane-local" aria-label="This computer" data-i18n-aria-label>
<div class="fx__bar">
<div class="fx__where" id="local-where"></div>
<nav class="crumbs" id="local-crumbs" aria-label="Where you are" data-i18n-aria-label></nav>
<div class="fx__tools">
<button type="button" class="tool" id="local-refresh" aria-label="Read again" title="Read again" data-i18n-aria-label data-i18n-title></button>
</div>
</div>
<div class="fx__panes">
<aside class="fx__tree" id="local-tree" aria-label="Folders" data-i18n-aria-label></aside>
<div class="fx__list" id="local-list"></div>
<aside class="fx__details" id="local-details" aria-label="Details" data-i18n-aria-label></aside>
</div>
<div class="files__tree" id="local-tree"></div>
</section>
<section class="files__pane" id="pane-server" hidden aria-label="Server" data-i18n-aria-label>
<div class="files__head">
<div class="files__where muted" id="server-where"></div>
<button type="button" class="tool" id="server-upload" aria-label="Upload" title="Upload" data-i18n-aria-label data-i18n-title></button>
<button type="button" class="tool" id="server-mkdir" aria-label="New folder" title="New folder" data-i18n-aria-label data-i18n-title></button>
<button type="button" class="tool" id="server-refresh" aria-label="Read again" title="Read again" data-i18n-aria-label data-i18n-title></button>
<section class="fx" id="pane-server" hidden aria-label="Server" data-i18n-aria-label>
<div class="fx__bar">
<div class="fx__where"></div>
<nav class="crumbs" id="server-crumbs" aria-label="Where you are" data-i18n-aria-label></nav>
<div class="fx__tools">
<button type="button" class="tool" id="server-upload" aria-label="Upload" title="Upload" data-i18n-aria-label data-i18n-title></button>
<button type="button" class="tool" id="server-mkdir" aria-label="New folder" title="New folder" data-i18n-aria-label data-i18n-title></button>
<button type="button" class="tool" id="server-refresh" aria-label="Read again" title="Read again" data-i18n-aria-label data-i18n-title></button>
</div>
</div>
<p class="status fx__status" id="server-status" hidden></p>
<div class="fx__panes">
<aside class="fx__tree" id="server-tree" aria-label="Folders" data-i18n-aria-label></aside>
<div class="fx__list" id="server-list"></div>
<aside class="fx__details" id="server-details" aria-label="Details" data-i18n-aria-label></aside>
</div>
<p class="status" id="server-status" hidden></p>
<div class="files__tree" id="server-tree"></div>
</section>
</div>
</section>
+3
View File
@@ -104,6 +104,9 @@ export const ICONS = {
mic: ["M8 1.5a2.5 2.5 0 012.5 2.5v4a2.5 2.5 0 01-5 0V4A2.5 2.5 0 018 1.5zM8 3a1 1 0 00-1 1v4a1 1 0 002 0V4a1 1 0 00-1-1z", "M3.5 7.5H5a3 3 0 006 0h1.5a4.5 4.5 0 01-3.75 4.44V14h-1.5v-2.06A4.5 4.5 0 013.5 7.5z"],
speaker: ["M2 6h2.5L8 3v10L4.5 10H2z", "M10.2 5.2a4 4 0 010 5.6l-1.06-1.06a2.5 2.5 0 000-3.48z", "M12.3 3.1a7 7 0 010 9.8l-1.06-1.06a5.5 5.5 0 000-7.68z"],
picture: ["M1.5 2.5h13v11h-13zM3 4v6.4l2.75-2.75 2.5 2.5 1.75-1.75L13 11.4V4z", "M10.5 5a1.25 1.25 0 110 2.5 1.25 1.25 0 010-2.5z"],
fileText: ["M3 1.5h6.5L13 5v9.5H3zM4.5 3v10h7V6H8.5V3z", "M6 8h4v1H6zM6 10h4v1H6z"],
fileCode: ["M3 1.5h6.5L13 5v9.5H3zM4.5 3v10h7V6H8.5V3z", "M6.7 7.6l.7.7-1.2 1.2 1.2 1.2-.7.7-1.9-1.9z", "M9.3 7.6l1.9 1.9-1.9 1.9-.7-.7 1.2-1.2-1.2-1.2z"],
drive: ["M1.5 4h13v8h-13zM3 5.5v5h10v-5z", "M10.5 7.25h1.5v1.5h-1.5z"],
info: ["M8 1.5a6.5 6.5 0 110 13 6.5 6.5 0 010-13zM8 3a5 5 0 100 10A5 5 0 008 3z", "M7.25 7h1.5v4.5h-1.5zM7.25 4.5h1.5V6h-1.5z"],
}
+434 -140
View File
@@ -1,141 +1,376 @@
// The two explorers of the Files page, kept apart as two tabs: this computer's -- the agent's
// folders, read through a command that reaches only them (src-tauri/src/files.rs) -- and the
// server's tree, for the chats, read through /api/v2. A file opens in the viewer either way.
// The Files page: two explorers, kept apart as two tabs -- this computer's (the agent's folders,
// read through a command that reaches only them, src-tauri/src/files.rs) and the server's (the
// person's files, as the chats see them, read through /api/v2). Each is the web explorer's shape,
// using the window's width: where you are (crumbs) and what can be done there, the folder tree,
// the list (name, type, size, changed), and the chosen file's details and preview. A file opens in
// the viewer either way.
//
// Nothing here is ever an empty box: a directory being read says Loading…, past SLOW_MS it says
// the server has not answered (with Try again, the late answer still drawn), and a refusal, an
// answer that is not a listing or a failure to draw it is said in its place with Try again.
import { $, h, clear, icon, ICONS, bytes } from "./dom.js"
import { invoke, get, post, del, why } from "./bridge.js"
import { menu, ask } from "./popup.js"
import { openViewer } from "./viewer.js"
import { openViewer, showKind, withheldWords } from "./viewer.js"
// The server's tree as the app's door gives it, read as the person (their own access, no chat):
// GET /files?path=/a -> { path, parent, entries: [{ name, path, kind: "dir"|"file"|"mount", size, mime, updated_at }] }
// GET /files/content?path= -> { path, name, size, mime, kind, language, sha, text, withheld?: { reason, message } }
// A path the person cannot read is the server's 404, in its words.
// A path the person cannot read is the server's 404, in its words. `/` answers with where the
// person's explorer opens (`fs.start`): `/` itself for an organisation's owner or administrator,
// the person's home for anyone else -- its `path` says which.
export const SERVER_FILES = {
list: (path) => `/files?path=${encodeURIComponent(path || "/")}`,
read: (path) => `/files/content?path=${encodeURIComponent(path)}`,
}
/** A directory in either tree: the server's mounts open like its folders. */
const isDir = (e) => e.kind === "dir" || e.kind === "mount"
/** How long a directory may take before the pane says it is still waiting, and offers to ask again. */
export const SLOW_MS = 8000
/**
* A lazily opened tree in `box`. `list(path)` -> entries; `open(entry)` for a file; `actions(entry)`
* (optional) -> menu items for an entry's more button. Returns `reload(path)`, which reads a
* directory that is open again (the root by `rootPath`), the folders open inside it kept open.
*
* A directory is never an empty box: while it is read it says "Loading…" (and, past SLOW_MS, that
* the answer has not come, with Try again); a refusal, or anything that breaks drawing it, is said
* in its place with Try again. Only the latest reading of a directory is drawn: an answer that
* comes after the directory was read again, closed or redrawn is dropped, not appended.
*/
function tree(box, { list, open, rootPath, actions }) {
clear(box)
const ul = h("ul", { class: "tree", role: "tree" })
box.append(ul)
const levels = new Map() // path -> { ul, depth }, each directory drawn now
const opened = new Set() // the directories open now
const asked = new WeakMap() // ul -> the reading it shows now (a number)
const level = async (parent, path, depth) => {
levels.set(path, { ul: parent, depth })
const mine = (asked.get(parent) || 0) + 1
asked.set(parent, mine)
const stale = () => asked.get(parent) !== mine || !parent.isConnected
const again = () => {
clear(parent)
return level(parent, path, depth)
}
const retry = () => h("button", { type: "button", class: "btn btn--sm tree__retry", text: t("Try again"), onclick: again })
const loading = h("li", { class: "tree__note is-loading", role: "status", "aria-live": "polite" }, h("span", { text: t("Loading…") }))
loading.style.setProperty("--depth", String(depth))
parent.append(loading)
const slow = setTimeout(() => {
if (stale()) return
loading.append(h("span", { text: ` ${t("The server has not answered in %(n)s seconds.", { n: Math.round(SLOW_MS / 1000) })}` }), retry())
}, SLOW_MS)
const said = (text) => {
const note = h("li", { class: "tree__note is-error", role: "alert" }, h("span", { text }), retry())
note.style.setProperty("--depth", String(depth))
return note
}
let entries
try {
entries = await list(path)
} catch (err) {
clearTimeout(slow)
if (!stale()) loading.replaceWith(said(why(err)))
return
} finally {
clearTimeout(slow)
}
if (stale()) return
try {
if (!Array.isArray(entries)) throw { detail: t("The server's answer could not be read.") }
const items = []
if (!entries.length) items.push(h("li", { class: "tree__note", text: t("Empty folder") }))
for (const e of entries) items.push(item(e, depth))
loading.replaceWith(...items)
} catch (err) {
loading.replaceWith(said(err instanceof Error ? err.message : why(err)))
}
/** How long an answer is shared between the tree and the list after it came. */
const SHARED_MS = 1500
/** The most of a file the details pane previews: the viewer has the rest. */
const PREVIEW_CHARS = 6000
/** A directory in either tree: the server's mounts open like its folders. */
const isDir = (e) => e.kind === "dir" || e.kind === "mount"
const IMAGE = /\.(png|jpe?g|gif|webp|bmp|ico|avif|svg)$/i
/** An entry's icon, by what the viewer would make of it (the web's `file_icon`). */
function iconOf(e) {
if (e.kind === "mount") return ICONS.drive
if (isDir(e)) return ICONS.folder
if ((e.mime || "").startsWith("image/") || IMAGE.test(e.name)) return ICONS.picture
const { kind } = showKind(e.path || e.name)
return kind === "code" || kind === "html" ? ICONS.fileCode : kind === "markdown" || kind === "text" ? ICONS.fileText : ICONS.file
}
/** The Type column: a folder, a mount, or the extension (the web's `type_label`). */
function typeOf(e) {
if (e.kind === "mount") return t("mount")
if (isDir(e)) return t("folder")
const i = e.name.lastIndexOf(".")
return i > 0 ? e.name.slice(i + 1).toLowerCase() : ""
}
/** A stamp as a date the person reads, and the whole of it for a title. */
function dateOf(stamp) {
if (!stamp) return { short: "", full: "" }
const d = new Date(stamp)
if (Number.isNaN(d.getTime())) return { short: "", full: "" }
const lang = document.documentElement.lang || undefined
return {
short: new Intl.DateTimeFormat(lang, { day: "numeric", month: "short", year: "numeric" }).format(d),
full: new Intl.DateTimeFormat(lang, { dateStyle: "long", timeStyle: "short" }).format(d),
}
const item = (e, depth) => {
}
/** Is the details pane on the screen (wide windows): a file is chosen there, else opened. */
const detailsShown = (el) => !!el && getComputedStyle(el).display !== "none"
/**
* Read something into `holder` and draw it, never leaving it empty: Loading… at once, the slow
* note and Try again past SLOW_MS, a refusal or a failure to draw in its place. `tag` is the
* element the notes are (an `li` inside a list). Only the latest reading of a holder is drawn:
* one that comes after the holder was read again, or left the page, is dropped. Returns a promise
* that settles once the reading is drawn or said.
*/
function reading(holder, { tag = "div", depth = 0, load, draw }) {
const mine = (readings.get(holder) || 0) + 1
readings.set(holder, mine)
const stale = () => readings.get(holder) !== mine || !holder.isConnected
const again = () => reading(holder, { tag, depth, load, draw })
const retry = () => h("button", { type: "button", class: "btn btn--sm fnote__retry", text: t("Try again"), onclick: again })
const note = (cls, role, text) => {
const n = h(tag, { class: `fnote ${cls}`, role }, h("span", { text }))
n.style.setProperty("--depth", String(depth))
return n
}
clear(holder)
const loading = note("is-loading", "status", t("Loading…"))
loading.setAttribute("aria-live", "polite")
holder.append(loading)
const slow = setTimeout(() => {
if (!stale()) loading.append(h("span", { text: t("The server has not answered in %(n)s seconds.", { n: Math.round(SLOW_MS / 1000) }) }), retry())
}, SLOW_MS)
const said = (text) => {
const n = note("is-error", "alert", text)
n.append(retry())
clear(holder).append(n)
}
return Promise.resolve()
.then(load)
.then(
(answer) => {
clearTimeout(slow)
if (stale()) return
try {
clear(holder)
draw(answer)
} catch (err) {
said(err instanceof Error ? err.message : why(err))
}
},
(err) => {
clearTimeout(slow)
if (!stale()) said(why(err))
},
)
}
const readings = new WeakMap() // holder -> the reading it shows now (a number)
/**
* One explorer, inside the pane's elements (`crumbs`, `tree`, `list`, `details`). `source`:
* key "local" | "server" (the rows' data, the tests' handle)
* list(path) -> { path, entries } (`path` as answered: the server's `/` says where it is)
* top() the directory the explorer opens at (the server's: as `/` answered)
* ask(path) the path to ask for a directory (the server's top is asked as `/`)
* crumbs(path) -> [[name, path], ...] from the top down
* rootName() the tree's first line
* open(entry) in the viewer
* preview(entry) -> { text, withheld, size }
* actions(entry) (optional) -> menu items
*/
function explorer(el, source) {
let current = null // the directory listed now (as answered)
let chosen = null // the file whose details show
// One request per directory for the tree and the list together: an answer is shared while it
// is on its way and for a moment after (the tree drawn again once the top is known); a refusal
// is never kept, so Try again asks afresh. Read again and a change forget them all.
const pending = new Map() // asked path -> promise
const fetchDir = (path) => {
const asked = source.ask(path)
if (!pending.has(asked)) {
const p = Promise.resolve().then(() => source.list(asked))
pending.set(asked, p)
const forget = () => pending.get(asked) === p && pending.delete(asked)
p.then(() => setTimeout(forget, SHARED_MS), forget)
}
return pending.get(asked)
}
// --- The folder tree: folders only, opened lazily; a name lists its folder.
const treeBox = el.tree
const opened = new Set()
const branches = new Map() // path -> { ul, depth }
const drawTree = () => {
clear(treeBox)
const rootRow = h(
"button",
{ type: "button", class: "ftree__row ftree__root", dataset: { path: source.top() }, onclick: () => go(source.top()) },
icon(ICONS.folder),
h("span", { class: "ftree__name", text: source.rootName() }),
)
const ul = h("ul", { class: "ftree", role: "tree", "aria-label": t("Folders") })
treeBox.append(rootRow, ul)
branch(ul, source.top(), 0)
mark()
}
const branch = (ul, path, depth) => {
branches.set(path, { ul, depth })
return reading(ul, {
tag: "li",
depth,
load: () => fetchDir(path),
draw: (answer) => {
const dirs = (answer.entries || []).filter(isDir)
for (const e of dirs) ul.append(twig(e, depth))
mark()
},
})
}
const twig = (e, depth) => {
const chevron = h("button", { type: "button", class: "ftree__chevron", "aria-label": e.name, "aria-expanded": "false", tabindex: "-1" }, icon(ICONS.chevron))
const row = h(
"button",
{ type: "button", class: "tree__row", title: e.path, "aria-expanded": isDir(e) ? "false" : null },
h("span", { class: "tree__chevron" }, isDir(e) ? icon(ICONS.chevron) : null),
icon(isDir(e) ? ICONS.folder : ICONS.file),
h("span", { class: "tree__name", text: e.name }),
h("span", { class: "tree__size", text: isDir(e) ? "" : bytes(e.size) }),
{ type: "button", class: "ftree__row", title: e.path, dataset: { path: e.path } },
icon(iconOf(e)),
h("span", { class: "ftree__name", text: e.name }),
)
row.style.setProperty("--depth", String(depth))
const items = actions?.(e) || []
const li = h(
"li",
{ role: "treeitem", class: items.length ? "tree__item" : null, dataset: { path: e.path } },
row,
items.length
? h("button", { type: "button", class: "tool tree__more", "aria-label": t("More"), title: t("More"), onclick: (ev) => menu(ev.currentTarget, items) }, icon(ICONS.more))
: null,
)
if (isDir(e)) {
let kids = null
const expand = async (opening) => {
row.setAttribute("aria-expanded", String(opening))
if (!opening) {
opened.delete(e.path)
levels.delete(e.path)
kids?.remove()
kids = null
return
}
opened.add(e.path)
kids?.remove()
kids = h("ul", { role: "group" })
li.append(kids)
await level(kids, e.path, depth + 1)
const line = h("div", { class: "ftree__line" }, chevron, row)
line.style.setProperty("--depth", String(depth))
const li = h("li", { role: "treeitem", dataset: { path: e.path } }, line)
let kids = null
const expand = (opening) => {
chevron.setAttribute("aria-expanded", String(opening))
li.setAttribute("aria-expanded", String(opening))
kids?.remove()
kids = null
if (!opening) {
opened.delete(e.path)
branches.delete(e.path)
return
}
row.addEventListener("click", () => expand(row.getAttribute("aria-expanded") !== "true"))
if (opened.has(e.path)) expand(true)
} else row.addEventListener("click", () => open(e))
opened.add(e.path)
kids = h("ul", { role: "group" })
li.append(kids)
branch(kids, e.path, depth + 1)
}
chevron.addEventListener("click", () => expand(chevron.getAttribute("aria-expanded") !== "true"))
row.addEventListener("click", () => {
go(e.path)
if (chevron.getAttribute("aria-expanded") !== "true") expand(true)
})
if (opened.has(e.path)) expand(true)
return li
}
const done = level(ul, rootPath, 0)
const reload = (path) => {
const at = levels.get(path) || levels.get(rootPath)
if (!at) return Promise.resolve()
clear(at.ul)
return level(at.ul, levels.has(path) ? path : rootPath, at.depth)
/** The tree's line for the folder listed now. */
const mark = () => {
for (const r of treeBox.querySelectorAll(".ftree__row")) {
const here = r.dataset.path === current || (r.classList.contains("ftree__root") && current === source.top())
r.classList.toggle("is-current", here)
if (here) r.setAttribute("aria-current", "true")
else r.removeAttribute("aria-current")
}
}
return { done, reload }
// --- Where you are.
const drawCrumbs = () => {
const parts = source.crumbs(current ?? source.top())
clear(el.crumbs).append(
...parts.flatMap(([name, at], i) => {
const last = i === parts.length - 1
const part = last
? h("span", { class: "crumbs__here", "aria-current": "page", title: at, text: name })
: h("button", { type: "button", class: "crumbs__link", title: at, text: name, onclick: () => go(at) })
return i ? [h("span", { class: "crumbs__sep", "aria-hidden": "true", text: "/" }), part] : [part]
}),
)
el.crumbs.scrollLeft = el.crumbs.scrollWidth
}
// --- The list.
const go = (path) => {
current = path
chosen = null
drawDetails()
drawCrumbs()
mark()
return reading(el.list, {
load: () => fetchDir(path),
draw: (answer) => {
if (!answer || !Array.isArray(answer.entries)) throw { detail: t("The server's answer could not be read.") }
if (answer.path !== undefined && answer.path !== path && source.ask(path) === source.ask(answer.path)) current = answer.path
drawCrumbs()
mark()
drawRows(answer.entries)
},
})
}
const drawRows = (entries) => {
if (!entries.length) {
el.list.append(h("div", { class: "fx__empty" }, icon(ICONS.folder), h("p", { text: t("Nothing here yet. Not even a hobbit hole.") })))
return
}
const head = h(
"div",
{ class: "frow frow--head", role: "row" },
h("span", { role: "columnheader", text: t("Name") }),
h("span", { role: "columnheader", class: "frow__type", text: t("Type") }),
h("span", { role: "columnheader", class: "frow__size", text: t("Size") }),
h("span", { role: "columnheader", class: "frow__when", text: t("Changed") }),
h("span", { role: "columnheader" }, h("span", { class: "visually-hidden", text: t("Actions") })),
)
el.list.append(h("div", { class: "frows", role: "table", "aria-label": t("Contents") }, head, entries.map(row)))
}
const row = (e) => {
const date = dateOf(e.updated_at)
const items = source.actions?.(e) || []
const openRow = h(
"button",
{ type: "button", class: "frow__open", title: e.path },
h("span", { class: `frow__icon frow__icon--${e.kind === "mount" ? "mount" : isDir(e) ? "dir" : showKind(e.name).kind}` }, icon(iconOf(e))),
h("span", { class: "frow__label", text: e.name }),
)
const r = h(
"div",
{ class: "frow", role: "row", dataset: { path: e.path, kind: e.kind } },
h("span", { role: "cell", class: "frow__name" }, openRow, e.kind === "mount" ? h("span", { class: "badge", text: t("mount") }) : null),
h("span", { role: "cell", class: "frow__type", text: typeOf(e) }),
h("span", { role: "cell", class: "frow__size", text: isDir(e) ? "" : bytes(e.size) }),
h("span", { role: "cell", class: "frow__when", title: date.full, text: date.short }),
h(
"span",
{ role: "cell", class: "frow__actions" },
items.length
? h("button", { type: "button", class: "tool frow__more", "aria-label": t("More for %(name)s", { name: e.name }), title: t("More for %(name)s", { name: e.name }), onclick: (ev) => menu(ev.currentTarget, items) }, icon(ICONS.more))
: null,
),
)
openRow.addEventListener("click", () => {
if (isDir(e)) go(e.path)
else if (detailsShown(el.details)) choose(e)
else source.open(e)
})
openRow.addEventListener("dblclick", () => {
if (!isDir(e)) source.open(e)
})
return r
}
// --- The chosen file's details and preview.
const choose = (e) => {
chosen = e
for (const r of el.list.querySelectorAll(".frow[data-path]")) r.classList.toggle("is-chosen", r.dataset.path === e.path)
drawDetails()
}
const drawDetails = () => {
const box = el.details
if (!box) return
for (const r of el.list.querySelectorAll(".frow.is-chosen")) if (!chosen || r.dataset.path !== chosen.path) r.classList.remove("is-chosen")
clear(box)
if (!chosen) {
box.append(h("div", { class: "fdetails__none" }, icon(ICONS.file), h("p", { text: t("Choose a file to see it here.") })))
return
}
const e = chosen
const date = dateOf(e.updated_at)
const fact = (label, value, title) => (value ? [h("dt", { text: label }), h("dd", { title: title || value, text: value })] : [])
const preview = h("div", { class: "fdetails__preview", "aria-label": t("Preview") })
box.append(
h("div", { class: "fdetails__id" }, h("span", { class: `frow__icon frow__icon--${showKind(e.name).kind}` }, icon(iconOf(e))), h("h2", { class: "fdetails__name", title: e.name, text: e.name })),
h("dl", { class: "fdetails__facts" }, fact(t("Type"), typeOf(e)), fact(t("Size"), bytes(e.size)), fact(t("Changed"), date.short, date.full), fact(t("Path"), e.path)),
h("div", { class: "fdetails__do" }, h("button", { type: "button", class: "btn btn--sm btn--primary", text: t("Open in the viewer"), onclick: () => source.open(e) })),
preview,
)
reading(preview, {
load: () => source.preview(e),
draw: (f) => {
if (chosen !== e) return
if (f.withheld) preview.append(h("p", { class: "fdetails__note", text: withheldWords(f.withheld, f.size ?? e.size) }))
else {
const text = String(f.text ?? "")
preview.append(h("pre", { class: "fdetails__text", text: text.length > PREVIEW_CHARS ? `${text.slice(0, PREVIEW_CHARS)}\n…` : text }))
}
},
})
}
/** Read again: the list, the tree as it is open, the file chosen. */
const refresh = () => {
pending.clear()
const at = current ?? source.top()
drawTree()
return go(at)
}
/** A directory changed (an upload, a new folder, a rename, a delete): draw what is there now. */
const changed = (dir) => {
pending.clear()
const b = branches.get(dir)
if (b) branch(b.ul, dir, b.depth)
if (dir === current || source.ask(dir) === source.ask(current ?? source.top())) return go(current ?? source.top())
return Promise.resolve()
}
return { go, refresh, changed, drawTree, current: () => current ?? source.top() }
}
// --- This computer ------------------------------------------------------------------------------
let local = { root: "", roots: [], fixed: false }
let localPane = null
/**
* Point this computer's pane at a folder: `fixed` (an agent chat's working directory, which is
@@ -147,32 +382,63 @@ export function showLocal({ roots, root, fixed }) {
if (changed) renderLocal()
}
const baseName = (p) => String(p).replace(/\/+$/, "").split("/").pop() || p
function renderLocal() {
const head = clear($("local-where"))
const box = $("local-tree")
const els = { crumbs: $("local-crumbs"), tree: $("local-tree"), list: $("local-list"), details: $("local-details") }
$("pane-local").dataset.empty = String(!local.roots.length)
if (!local.roots.length) {
clear(box).append(
h("p", { class: "pane__note", text: t("No project folder yet: the agent can open nothing on this computer.") }),
h("button", { type: "button", class: "btn btn--sm btn--primary", text: t("Add folder…"), onclick: () => invoke("remote_add") }),
clear(els.crumbs)
clear(els.tree)
clear(els.details)
clear(els.list).append(
h(
"div",
{ class: "fx__empty" },
icon(ICONS.folder),
h("p", { text: t("No project folder yet: the agent can open nothing on this computer.") }),
h("button", { type: "button", class: "btn btn--sm btn--primary", text: t("Add folder…"), onclick: () => invoke("remote_add") }),
),
)
localPane = null
return
}
if (local.fixed) {
head.append(h("span", { class: "pane__path", title: local.root }, h("bdi", { text: local.root })))
} else {
const pick = h(
"select",
{ class: "pane__pick", "aria-label": t("Folder"), onchange: (e) => showLocal({ roots: local.roots, root: e.target.value, fixed: false }) },
local.roots.map((r) => h("option", { value: r, text: r, selected: r === local.root })),
if (!local.fixed && local.roots.length > 1) {
head.append(
h(
"select",
{ class: "pane__pick", "aria-label": t("Folder"), title: local.root, onchange: (e) => showLocal({ roots: local.roots, root: e.target.value, fixed: false }) },
local.roots.map((r) => h("option", { value: r, text: r, selected: r === local.root })),
),
)
head.append(pick)
}
const root = local.root
return tree(box, {
rootPath: "",
list: async (path) => (await invoke("local_list", { root, path })).entries,
const where = t("This computer")
localPane = explorer(els, {
key: "local",
top: () => "",
ask: (path) => path || "",
rootName: () => baseName(root),
crumbs: (path) => {
const out = [[baseName(root), ""]]
let at = ""
for (const part of String(path).split("/").filter(Boolean)) {
at = at ? `${at}/${part}` : part
out.push([part, at])
}
return out
},
list: async (path) => {
const r = await invoke("local_list", { root, path })
if (!r || !Array.isArray(r.entries)) throw { detail: t("The server's answer could not be read.") }
return {
path: r.path ?? path,
entries: r.entries.map((e) => ({ ...e, updated_at: e.modified ? new Date(e.modified * 1000).toISOString() : null })),
}
},
preview: (e) => invoke("local_read", { root, path: e.path }),
open: async (e) => {
const where = t("This computer")
try {
const f = await invoke("local_read", { root, path: e.path })
openViewer({ name: f.name, path: `${f.root}/${f.path}`, where, text: f.text, withheld: f.withheld, size: f.size })
@@ -181,6 +447,8 @@ function renderLocal() {
}
},
})
localPane.drawTree()
localPane.go("")
}
/** Read a file the agent showed in a chat on this computer: `path` from the chat's folder. */
@@ -201,11 +469,11 @@ export async function readServer(path) {
// POST /files/rename { path, name }
// DELETE /files?path= to the trash, as on the web
let server = null // the tree drawn now: { reload(path) }
let serverRoot = "/" // the directory the server answered `/` with: the person's home
let server = null // the explorer drawn now
let serverRoot = "/" // the directory the server answered `/` with: `/` or the person's home
const parentOf = (path) => path.replace(/\/[^/]*$/, "") || "/"
const reloadServer = (dir) => server?.reload(dir === serverRoot ? "/" : dir)
const reloadServer = (dir) => server?.changed(dir)
function serverSaid(text, error = false) {
const line = $("server-status")
@@ -268,24 +536,48 @@ async function deleteEntry(e) {
function serverActions(e) {
const items = []
if (isDir(e)) items.push({ label: t("Upload"), run: () => uploadInto(e.path) }, { label: t("New folder"), run: () => makeFolder(e.path) })
if (e.kind !== "mount") items.push(...(items.length ? [null] : []), { label: t("Rename"), run: () => renameEntry(e) }, { label: t("Delete"), danger: true, run: () => deleteEntry(e) })
else items.push({ label: t("Open in the viewer"), run: () => openServerFile(e.path, e.name) })
if (e.kind !== "mount") items.push(null, { label: t("Rename"), run: () => renameEntry(e) }, { label: t("Delete"), danger: true, run: () => deleteEntry(e) })
return items
}
export function renderServer() {
const box = $("server-tree")
serverSaid("")
server = tree(box, {
rootPath: "/",
const els = { crumbs: $("server-crumbs"), tree: $("server-tree"), list: $("server-list"), details: $("server-details") }
let first = true
server = explorer(els, {
key: "server",
top: () => serverRoot,
// The top is asked as `/`, which is what the server opens a person at.
ask: (path) => (!path || path === serverRoot ? "/" : path),
rootName: () => (serverRoot === "/" ? t("All files") : t("Home")),
crumbs: (path) => {
const out = [[serverRoot === "/" ? t("All files") : t("Home"), serverRoot]]
const rest = path.startsWith(serverRoot) ? path.slice(serverRoot.length) : path
let at = serverRoot
for (const part of rest.split("/").filter(Boolean)) {
at = `${at.replace(/\/$/, "")}/${part}`
out.push([part, at])
}
return out
},
list: async (path) => {
const r = await get(SERVER_FILES.list(path))
if (!r || !Array.isArray(r.entries)) throw { detail: t("The server's answer could not be read.") }
if (path === "/" && r.path) serverRoot = r.path
return r.entries
if (path === "/" && r.path && r.path !== serverRoot) {
serverRoot = r.path
// The top is known now: the tree and the crumbs start there.
if (first) queueMicrotask(() => server?.drawTree())
}
first = false
return r
},
preview: (e) => readServer(e.path),
open: (e) => openServerFile(e.path, e.name),
actions: serverActions,
})
server.drawTree()
return server.go(serverRoot)
}
/** A file in the server's tree, in the viewer: from the explorer and from a chat's outputs. */
@@ -312,7 +604,7 @@ export async function openServerFile(path, name) {
let tab = "local"
let serverRead = false
/** Show one of the two trees: `local` (this computer) or `server`. The server's is read once. */
/** Show one of the two explorers: `local` (this computer) or `server`. The server's is read once. */
export function showFilesTab(which) {
tab = which === "server" ? "server" : "local"
for (const b of $("files-tabs").querySelectorAll("button")) b.setAttribute("aria-selected", String(b.dataset.tab === tab))
@@ -328,13 +620,15 @@ export function initExplorer() {
const tabBtn = (which, ico, label) =>
h("button", { type: "button", role: "tab", class: "tabs__tab", dataset: { tab: which }, "aria-selected": String(which === tab), onclick: () => showFilesTab(which) }, icon(ico), h("span", { text: label }))
$("files-tabs").replaceChildren(tabBtn("local", ICONS.folder, t("This computer")), tabBtn("server", ICONS.file, t("Server")))
$("server-where").textContent = t("Your files on the server, as the chats see them.")
$("server-refresh").replaceChildren(icon(ICONS.refresh))
$("server-refresh").addEventListener("click", renderServer)
$("server-refresh").addEventListener("click", () => {
serverSaid("")
return server ? server.refresh() : renderServer()
})
$("server-upload").replaceChildren(icon(ICONS.send))
$("server-upload").addEventListener("click", () => uploadInto(serverRoot))
$("server-upload").addEventListener("click", () => uploadInto(server?.current() ?? serverRoot))
$("server-mkdir").replaceChildren(icon(ICONS.folder))
$("server-mkdir").addEventListener("click", () => makeFolder(serverRoot))
$("server-mkdir").addEventListener("click", () => makeFolder(server?.current() ?? serverRoot))
$("local-refresh").replaceChildren(icon(ICONS.refresh))
$("local-refresh").addEventListener("click", renderLocal)
$("local-refresh").addEventListener("click", () => (localPane ? localPane.refresh() : renderLocal()))
}
+1 -1
View File
@@ -52,7 +52,7 @@ export function closeViewer() {
* server's is { reason: "not_text" | "too_large", message } with the message in the person's
* language already.
*/
function withheldWords(withheld, size) {
export function withheldWords(withheld, size) {
if (typeof withheld === "object" && withheld.message) return String(withheld.message)
const reason = typeof withheld === "object" ? withheld.reason : withheld
return reason === "too_big" || reason === "too_large"