Library upload, new folder, rename and delete; a chat's tools and skills (/scope)
ci / ui (push) Waiting to run
ci / check (push) Failing after 4s
ci / start (push) Failing after 4s

Files -> Server writes through /api/v2/files/{upload,mkdir,rename} and
DELETE /files (the trash); upload is a Rust command (library_upload) that
picks files in the system dialog and sends them multipart in the field
`files`. A chat's menu opens Tools and skills..., the web's chat-settings
switches over GET/POST /chats/{id}/scope and /allow/clear. A popover can
open at a context menu's point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-10 13:53:38 +00:00
1 parent d691a4dcfa
commit 67ce6076c5
13 files changed
+582 -27

No files matched your search

+20
View File
@@ -1401,6 +1401,9 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
.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); }
@@ -1434,6 +1437,12 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
.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); font-size: var(--text-xs); color: var(--ink-muted); }
/* An empty page: an icon, a heading, a sentence, centred. */
@@ -1696,6 +1705,17 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
.arch__page { display: grid; gap: var(--sp-3); }
.head__unarchive { white-space: nowrap; }
/* --- A chat's tools and skills (scope.js): switches in a popover, the allowed commands below. */
.scope { max-height: min(70vh, 32rem); overflow-y: auto; }
.scope .pop__actions { justify-content: flex-start; }
.scope .pop__actions .btn { max-width: 100%; white-space: normal; height: auto; min-height: var(--control-h-sm); }
.scope__list { display: grid; gap: var(--sp-2); }
.scope__row span { overflow-wrap: anywhere; }
.scope__hint { margin: 0; font-size: var(--text-xs); }
.scope__allow { display: grid; gap: var(--sp-1); margin: 0; padding: 0; list-style: none; }
.scope__allow code { font: var(--text-xs) var(--font-mono); overflow-wrap: anywhere; }
/* --- Memory: a new one on top, then each record over its group and date · Edit · more. */
.mnew { display: grid; gap: var(--sp-2); }
+80
View File
@@ -53,6 +53,7 @@ const LEMBAS_I18N = {
"Allow": "Povolit",
"Always": "Vždy",
"Always allow": "Vždy povolit",
"Always allowed here": "Tady vždy povoleno",
"An agent chat works in a folder on %(device)s, through the LLeMbas agent there.": "Agentní konverzace pracuje ve složce na zařízení %(device)s přes tamního agenta LLeMbas.",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Agentní konverzace pracuje ve složce na tomto počítači přes agenta vestavěného v této aplikaci.",
"Another device": "Jiné zařízení",
@@ -61,6 +62,7 @@ const LEMBAS_I18N = {
"Approve this code in the page that opened in your browser:": "Potvrďte tento kód na stránce, která se otevřela ve vašem prohlížeči:",
"Archive chat": "Archivovat konverzaci",
"Archived": "Archivované",
"Ask me about these again": "Znovu se mě na to ptát",
"Attach files": "Přikládat soubory",
"Auto — asks nothing": "Auto — na nic se neptá",
"Automatic compaction is off.": "Automatické shrnutí je vypnuté.",
@@ -183,6 +185,9 @@ const LEMBAS_I18N = {
"Models for scheduled tasks": "Modely pro naplánované úlohy",
"More": "Více",
"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.",
"Never": "Nikdy",
"New agent chat": "Nová agentní konverzace",
"New chat": "Nová konverzace",
@@ -214,6 +219,7 @@ const LEMBAS_I18N = {
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Na pozadí nic neběží. Podagenti, členové týmu a příkazy na pozadí se tu zobrazí, dokud pracují.",
"Nothing to report": "Nic k hlášení",
"Nothing to send: select some output first.": "Není co odeslat: nejdřív označte část výstupu.",
"Nothing to switch in this chat.": "V této konverzaci není co přepnout.",
"Nothing yet.": "Zatím nic.",
"Notifications": "Upozornění",
"One chat that grows": "Jedna konverzace, která roste",
@@ -293,6 +299,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",
"Skills": "Dovednosti",
"Skipped": "Vynecháno",
"Something to remember…": "Něco k zapamatování…",
"Source": "Zdroj",
@@ -309,6 +316,7 @@ const LEMBAS_I18N = {
"Subagent %(name)s (%(id)s), %(position)s, asks:": "Podagent %(name)s (%(id)s), %(position)s, se ptá:",
"Subagents": "Podagenti",
"Summarise the earlier turns now and send the summary in their place.": "Shrnout dřívější kola hned a posílat místo nich shrnutí.",
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Vypnuto jen tady. Co je na začátku zapnuté, je vaše výchozí nastavení pro tento model v Nastavení → Modely.",
"System prompt": "Systémový pokyn",
"Taken in while it ran:": "Převzato během běhu:",
"Tasks: %(n)s": "Úlohy: %(n)s",
@@ -374,11 +382,13 @@ const LEMBAS_I18N = {
"Title": "Název",
"Today": "Dnes",
"Tools": "Nástroje",
"Tools and skills…": "Nástroje a dovednosti…",
"Try again": "Zkusit znovu",
"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",
"Up to %(n)s at once": "Nejvýše %(n)s současně",
"Upload": "Nahrát",
"Wait until the files are sent.": "Počkejte, až se soubory odešlou.",
"Waiting for a free model": "Čeká na volný model",
"Waiting for subagents: {{running}} running": "Čeká na podagenty: pracuje {{running}}",
@@ -493,6 +503,7 @@ const LEMBAS_I18N = {
"Allow": "Erlauben",
"Always": "Immer",
"Always allow": "Immer erlauben",
"Always allowed here": "Hier immer erlaubt",
"An agent chat works in a folder on %(device)s, through the LLeMbas agent there.": "Ein Agenten-Chat arbeitet in einem Ordner auf %(device)s, über den LLeMbas-Agenten dort.",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Ein Agenten-Chat arbeitet in einem Ordner auf diesem Computer, über den in diese App eingebauten Agenten.",
"Another device": "Ein anderes Gerät",
@@ -501,6 +512,7 @@ const LEMBAS_I18N = {
"Approve this code in the page that opened in your browser:": "Bestätigen Sie diesen Code auf der Seite, die sich in Ihrem Browser geöffnet hat:",
"Archive chat": "Chat archivieren",
"Archived": "Archiviert",
"Ask me about these again": "Mich dazu wieder fragen",
"Attach files": "Dateien anhängen",
"Auto — asks nothing": "Auto — fragt nichts",
"Automatic compaction is off.": "Die automatische Zusammenfassung ist aus.",
@@ -623,6 +635,9 @@ const LEMBAS_I18N = {
"Models for scheduled tasks": "Modelle für geplante Aufgaben",
"More": "Mehr",
"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.",
"Never": "Nie",
"New agent chat": "Neuer Agenten-Chat",
"New chat": "Neuer Chat",
@@ -654,6 +669,7 @@ const LEMBAS_I18N = {
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Im Hintergrund läuft nichts. Subagenten, die Mitglieder eines Teams und Hintergrundbefehle erscheinen hier, solange sie arbeiten.",
"Nothing to report": "Nichts zu melden",
"Nothing to send: select some output first.": "Nichts zu senden: Markieren Sie zuerst etwas Ausgabe.",
"Nothing to switch in this chat.": "In diesem Chat gibt es nichts umzuschalten.",
"Nothing yet.": "Noch nichts.",
"Notifications": "Benachrichtigungen",
"One chat that grows": "Ein Chat, der wächst",
@@ -733,6 +749,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",
"Skills": "Fertigkeiten",
"Skipped": "Ausgelassen",
"Something to remember…": "Etwas, das behalten werden soll…",
"Source": "Quelltext",
@@ -749,6 +766,7 @@ const LEMBAS_I18N = {
"Subagent %(name)s (%(id)s), %(position)s, asks:": "Subagent %(name)s (%(id)s), %(position)s, fragt:",
"Subagents": "Subagenten",
"Summarise the earlier turns now and send the summary in their place.": "Die früheren Runden jetzt zusammenfassen und stattdessen die Zusammenfassung senden.",
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Nur hier ausgeschaltet. Was anfangs an ist, ist Ihr Standard für dieses Modell, unter Einstellungen → Modelle.",
"System prompt": "Systemprompt",
"Taken in while it ran:": "Während des Laufs übernommen:",
"Tasks: %(n)s": "Aufgaben: %(n)s",
@@ -814,11 +832,13 @@ const LEMBAS_I18N = {
"Title": "Titel",
"Today": "Heute",
"Tools": "Werkzeuge",
"Tools and skills…": "Werkzeuge und Fähigkeiten…",
"Try again": "Erneut versuchen",
"Type a command or a chat's title": "Befehl oder Chat-Titel eingeben",
"Type a folder…": "Ordner eingeben…",
"Unpin chat": "Chat loslösen",
"Up to %(n)s at once": "Bis zu %(n)s gleichzeitig",
"Upload": "Hochladen",
"Wait until the files are sent.": "Warten Sie, bis die Dateien gesendet sind.",
"Waiting for a free model": "Wartet auf ein freies Modell",
"Waiting for subagents: {{running}} running": "Wartet auf Subagenten: {{running}} laufen",
@@ -933,6 +953,7 @@ const LEMBAS_I18N = {
"Allow": "Permitir",
"Always": "Siempre",
"Always allow": "Permitir siempre",
"Always allowed here": "Siempre permitido aquí",
"An agent chat works in a folder on %(device)s, through the LLeMbas agent there.": "Un chat de agente trabaja en una carpeta de %(device)s, a través del agente LLeMbas de allí.",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Un chat de agente trabaja en una carpeta de este equipo, a través del agente integrado en esta aplicación.",
"Another device": "Otro dispositivo",
@@ -941,6 +962,7 @@ const LEMBAS_I18N = {
"Approve this code in the page that opened in your browser:": "Apruebe este código en la página que se abrió en su navegador:",
"Archive chat": "Archivar chat",
"Archived": "Archivados",
"Ask me about these again": "Volver a preguntarme por esto",
"Attach files": "Adjuntar archivos",
"Auto — asks nothing": "Auto — no pregunta nada",
"Automatic compaction is off.": "La compactación automática está desactivada.",
@@ -1063,6 +1085,9 @@ const LEMBAS_I18N = {
"Models for scheduled tasks": "Modelos para tareas programadas",
"More": "Má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.",
"Never": "Nunca",
"New agent chat": "Nuevo chat de agente",
"New chat": "Nuevo chat",
@@ -1094,6 +1119,7 @@ const LEMBAS_I18N = {
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "No hay nada en segundo plano. Los subagentes, los miembros de un equipo y los comandos en segundo plano aparecen aquí mientras trabajan.",
"Nothing to report": "Nada que informar",
"Nothing to send: select some output first.": "Nada que enviar: seleccione primero algo de la salida.",
"Nothing to switch in this chat.": "No hay nada que activar o desactivar en este chat.",
"Nothing yet.": "Aún nada.",
"Notifications": "Notificaciones",
"One chat that grows": "Un chat que va creciendo",
@@ -1173,6 +1199,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",
"Skills": "Habilidades",
"Skipped": "Omitida",
"Something to remember…": "Algo que recordar…",
"Source": "Código fuente",
@@ -1189,6 +1216,7 @@ const LEMBAS_I18N = {
"Subagent %(name)s (%(id)s), %(position)s, asks:": "El subagente %(name)s (%(id)s), %(position)s, pregunta:",
"Subagents": "Subagentes",
"Summarise the earlier turns now and send the summary in their place.": "Resumir ahora los turnos anteriores y enviar el resumen en su lugar.",
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Desactivado solo aquí. Lo que está activado al principio es su valor predeterminado para este modelo, en Ajustes → Modelos.",
"System prompt": "Prompt de sistema",
"Taken in while it ran:": "Incorporado mientras se ejecutaba:",
"Tasks: %(n)s": "Tareas: %(n)s",
@@ -1254,11 +1282,13 @@ const LEMBAS_I18N = {
"Title": "Título",
"Today": "Hoy",
"Tools": "Herramientas",
"Tools and skills…": "Herramientas y habilidades…",
"Try again": "Reintentar",
"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",
"Up to %(n)s at once": "Hasta %(n)s a la vez",
"Upload": "Subir",
"Wait until the files are sent.": "Espere a que se envíen los archivos.",
"Waiting for a free model": "Esperando un modelo libre",
"Waiting for subagents: {{running}} running": "Esperando a los subagentes: {{running}} en marcha",
@@ -1373,6 +1403,7 @@ const LEMBAS_I18N = {
"Allow": "Autoriser",
"Always": "Toujours",
"Always allow": "Toujours autoriser",
"Always allowed here": "Toujours autorisé ici",
"An agent chat works in a folder on %(device)s, through the LLeMbas agent there.": "Une discussion d'agent travaille dans un dossier de %(device)s, via l'agent LLeMbas qui s'y trouve.",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Une discussion d'agent travaille dans un dossier de cet ordinateur, via l'agent intégré à cette application.",
"Another device": "Un autre appareil",
@@ -1381,6 +1412,7 @@ const LEMBAS_I18N = {
"Approve this code in the page that opened in your browser:": "Approuvez ce code sur la page qui s'est ouverte dans votre navigateur :",
"Archive chat": "Archiver la conversation",
"Archived": "Archivées",
"Ask me about these again": "Me redemander pour ceux-ci",
"Attach files": "Joindre des fichiers",
"Auto — asks nothing": "Auto — ne demande rien",
"Automatic compaction is off.": "Le compactage automatique est désactivé.",
@@ -1503,6 +1535,9 @@ const LEMBAS_I18N = {
"Models for scheduled tasks": "Modèles pour les tâches planifiées",
"More": "Plus",
"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.",
"Never": "Jamais",
"New agent chat": "Nouvelle discussion d'agent",
"New chat": "Nouvelle conversation",
@@ -1534,6 +1569,7 @@ const LEMBAS_I18N = {
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Rien ne tourne en arrière-plan. Les sous-agents, les membres d’une équipe et les commandes en arrière-plan s’affichent ici pendant qu’ils travaillent.",
"Nothing to report": "Rien à signaler",
"Nothing to send: select some output first.": "Rien à envoyer : sélectionnez d'abord une partie de la sortie.",
"Nothing to switch in this chat.": "Rien à activer ou désactiver dans cette conversation.",
"Nothing yet.": "Rien pour l'instant.",
"Notifications": "Notifications",
"One chat that grows": "Une conversation qui grandit",
@@ -1613,6 +1649,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",
"Skills": "Compétences",
"Skipped": "Ignorée",
"Something to remember…": "Quelque chose à retenir…",
"Source": "Source",
@@ -1629,6 +1666,7 @@ const LEMBAS_I18N = {
"Subagent %(name)s (%(id)s), %(position)s, asks:": "Le sous-agent %(name)s (%(id)s), %(position)s, demande :",
"Subagents": "Sous-agents",
"Summarise the earlier turns now and send the summary in their place.": "Résumer maintenant les tours précédents et envoyer le résumé à leur place.",
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Désactivé ici seulement. Ce qui est activé au départ est votre réglage par défaut pour ce modèle, dans Paramètres → Modèles.",
"System prompt": "Prompt système",
"Taken in while it ran:": "Pris en compte pendant l'exécution :",
"Tasks: %(n)s": "Tâches : %(n)s",
@@ -1694,11 +1732,13 @@ const LEMBAS_I18N = {
"Title": "Titre",
"Today": "Aujourd’hui",
"Tools": "Outils",
"Tools and skills…": "Outils et compétences…",
"Try again": "Réessayer",
"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",
"Up to %(n)s at once": "Jusqu’à %(n)s à la fois",
"Upload": "Envoyer",
"Wait until the files are sent.": "Attendez que les fichiers soient envoyés.",
"Waiting for a free model": "En attente d’un modèle libre",
"Waiting for subagents: {{running}} running": "En attente des sous-agents : {{running}} en cours",
@@ -1813,6 +1853,7 @@ const LEMBAS_I18N = {
"Allow": "Engedélyezés",
"Always": "Mindig",
"Always allow": "Mindig engedélyez",
"Always allowed here": "Itt mindig engedélyezett",
"An agent chat works in a folder on %(device)s, through the LLeMbas agent there.": "Az ügynökcsevegés egy mappában dolgozik ezen: %(device)s, az ottani LLeMbas-ügynökön keresztül.",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Az ügynökcsevegés egy mappában dolgozik ezen a számítógépen, az alkalmazásba épített ügynökön keresztül.",
"Another device": "Másik eszköz",
@@ -1821,6 +1862,7 @@ const LEMBAS_I18N = {
"Approve this code in the page that opened in your browser:": "Hagyja jóvá ezt a kódot a böngészőjében megnyílt oldalon:",
"Archive chat": "Csevegés archiválása",
"Archived": "Archivált",
"Ask me about these again": "Ezekről kérdezzen meg újra",
"Attach files": "Fájlok csatolása",
"Auto — asks nothing": "Auto — semmit nem kérdez",
"Automatic compaction is off.": "Az automatikus tömörítés ki van kapcsolva.",
@@ -1943,6 +1985,9 @@ const LEMBAS_I18N = {
"Models for scheduled tasks": "Modellek az ütemezett feladatokhoz",
"More": "Több",
"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.",
"Never": "Soha",
"New agent chat": "Új ügynökcsevegés",
"New chat": "Új csevegés",
@@ -1974,6 +2019,7 @@ const LEMBAS_I18N = {
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "A háttérben semmi sem fut. Az alügynökök, egy csapat tagjai és a háttérparancsok itt jelennek meg, amíg dolgoznak.",
"Nothing to report": "Nincs jelentenivaló",
"Nothing to send: select some output first.": "Nincs mit küldeni: előbb jelöljön ki valamennyi kimenetet.",
"Nothing to switch in this chat.": "Ebben a beszélgetésben nincs mit átkapcsolni.",
"Nothing yet.": "Még semmi.",
"Notifications": "Értesítések",
"One chat that grows": "Egy beszélgetés, amely bővül",
@@ -2053,6 +2099,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",
"Skills": "Készségek",
"Skipped": "Kihagyva",
"Something to remember…": "Valami, amit meg kell jegyezni…",
"Source": "Forrás",
@@ -2069,6 +2116,7 @@ const LEMBAS_I18N = {
"Subagent %(name)s (%(id)s), %(position)s, asks:": "A(z) %(name)s alügynök (%(id)s), %(position)s, kérdezi:",
"Subagents": "Alügynökök",
"Summarise the earlier turns now and send the summary in their place.": "A korábbi köröket most összefoglalja, és helyettük az összefoglalót küldi.",
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Csak itt kikapcsolva. Hogy kezdetben mi van bekapcsolva, az ennél a modellnél az Ön alapértelmezése, a Beállítások → Modellek alatt.",
"System prompt": "Rendszerprompt",
"Taken in while it ran:": "Futás közben átvéve:",
"Tasks: %(n)s": "Feladatok: %(n)s",
@@ -2134,11 +2182,13 @@ const LEMBAS_I18N = {
"Title": "Cím",
"Today": "Ma",
"Tools": "Eszközök",
"Tools and skills…": "Eszközök és készségek…",
"Try again": "Újrapróbálás",
"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",
"Up to %(n)s at once": "Legfeljebb %(n)s egyszerre",
"Upload": "Feltöltés",
"Wait until the files are sent.": "Várjon, amíg a fájlok elküldődnek.",
"Waiting for a free model": "Szabad modellre vár",
"Waiting for subagents: {{running}} running": "Az alügynökökre vár: {{running}} dolgozik",
@@ -2253,6 +2303,7 @@ const LEMBAS_I18N = {
"Allow": "Consenti",
"Always": "Sempre",
"Always allow": "Consenti sempre",
"Always allowed here": "Sempre consentito qui",
"An agent chat works in a folder on %(device)s, through the LLeMbas agent there.": "Una chat con agente lavora in una cartella su %(device)s, tramite l’agente LLeMbas che vi si trova.",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Una chat con agente lavora in una cartella di questo computer, tramite l'agente integrato in questa app.",
"Another device": "Un altro dispositivo",
@@ -2261,6 +2312,7 @@ const LEMBAS_I18N = {
"Approve this code in the page that opened in your browser:": "Approvi questo codice nella pagina che si è aperta nel suo browser:",
"Archive chat": "Archivia chat",
"Archived": "Archiviate",
"Ask me about these again": "Chiedimi di nuovo per questi",
"Attach files": "Allegare file",
"Auto — asks nothing": "Auto — non chiede nulla",
"Automatic compaction is off.": "La compattazione automatica è disattivata.",
@@ -2383,6 +2435,9 @@ const LEMBAS_I18N = {
"Models for scheduled tasks": "Modelli per le attività pianificate",
"More": "Altro",
"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.",
"Never": "Mai",
"New agent chat": "Nuova chat con agente",
"New chat": "Nuova chat",
@@ -2414,6 +2469,7 @@ const LEMBAS_I18N = {
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Nulla è in esecuzione in background. Sottoagenti, membri di una squadra e comandi in background compaiono qui mentre lavorano.",
"Nothing to report": "Niente da segnalare",
"Nothing to send: select some output first.": "Niente da inviare: seleziona prima un po' di output.",
"Nothing to switch in this chat.": "Non c’è niente da attivare o disattivare in questa chat.",
"Nothing yet.": "Ancora niente.",
"Notifications": "Notifiche",
"One chat that grows": "Una chat che cresce",
@@ -2493,6 +2549,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",
"Skills": "Abilità",
"Skipped": "Saltata",
"Something to remember…": "Qualcosa da ricordare…",
"Source": "Sorgente",
@@ -2509,6 +2566,7 @@ const LEMBAS_I18N = {
"Subagent %(name)s (%(id)s), %(position)s, asks:": "Il subagente %(name)s (%(id)s), %(position)s, chiede:",
"Subagents": "Sottoagenti",
"Summarise the earlier turns now and send the summary in their place.": "Riassumi ora i turni precedenti e invia il riassunto al loro posto.",
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Disattivato solo qui. Ciò che è attivo all'inizio è il tuo predefinito per questo modello, in Impostazioni → Modelli.",
"System prompt": "Prompt di sistema",
"Taken in while it ran:": "Accolto durante l'esecuzione:",
"Tasks: %(n)s": "Attività: %(n)s",
@@ -2574,11 +2632,13 @@ const LEMBAS_I18N = {
"Title": "Titolo",
"Today": "Oggi",
"Tools": "Strumenti",
"Tools and skills…": "Strumenti e abilità…",
"Try again": "Riprova",
"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",
"Up to %(n)s at once": "Fino a %(n)s alla volta",
"Upload": "Carica",
"Wait until the files are sent.": "Attenda che i file siano inviati.",
"Waiting for a free model": "In attesa di un modello libero",
"Waiting for subagents: {{running}} running": "In attesa dei subagenti: {{running}} al lavoro",
@@ -2693,6 +2753,7 @@ const LEMBAS_I18N = {
"Allow": "Pozwól",
"Always": "Zawsze",
"Always allow": "Zawsze zezwalaj",
"Always allowed here": "Tutaj zawsze dozwolone",
"An agent chat works in a folder on %(device)s, through the LLeMbas agent there.": "Czat z agentem pracuje w folderze na %(device)s, przez działającego tam agenta LLeMbas.",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Czat z agentem pracuje w folderze na tym komputerze, przez agenta wbudowanego w tę aplikację.",
"Another device": "Inne urządzenie",
@@ -2701,6 +2762,7 @@ const LEMBAS_I18N = {
"Approve this code in the page that opened in your browser:": "Zatwierdź ten kod na stronie, która otworzyła się w przeglądarce:",
"Archive chat": "Archiwizuj czat",
"Archived": "Zarchiwizowane",
"Ask me about these again": "Pytaj o to ponownie",
"Attach files": "Załączaj pliki",
"Auto — asks nothing": "Auto — o nic nie pyta",
"Automatic compaction is off.": "Automatyczne streszczanie jest wyłączone.",
@@ -2823,6 +2885,9 @@ const LEMBAS_I18N = {
"Models for scheduled tasks": "Modele dla zaplanowanych zadań",
"More": "Więcej",
"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.",
"Never": "Nigdy",
"New agent chat": "Nowy czat z agentem",
"New chat": "Nowy czat",
@@ -2854,6 +2919,7 @@ const LEMBAS_I18N = {
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "W tle nic nie działa. Podagenci, członkowie zespołu i polecenia w tle pojawiają się tu, gdy pracują.",
"Nothing to report": "Nic do zgłoszenia",
"Nothing to send: select some output first.": "Nie ma czego wysłać: najpierw należy zaznaczyć jakiś wynik.",
"Nothing to switch in this chat.": "W tym czacie nie ma czego przełączać.",
"Nothing yet.": "Jeszcze nic.",
"Notifications": "Powiadomienia",
"One chat that grows": "Jeden czat, który rośnie",
@@ -2933,6 +2999,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",
"Skills": "Umiejętności",
"Skipped": "Pominięty",
"Something to remember…": "Coś do zapamiętania…",
"Source": "Źródło",
@@ -2949,6 +3016,7 @@ const LEMBAS_I18N = {
"Subagent %(name)s (%(id)s), %(position)s, asks:": "Podagent %(name)s (%(id)s), %(position)s, pyta:",
"Subagents": "Podagenci",
"Summarise the earlier turns now and send the summary in their place.": "Streścić teraz wcześniejsze tury i wysyłać streszczenie zamiast nich.",
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Wyłączone tylko tutaj. To, co jest początkowo włączone, to ustawienie domyślne dla tego modelu w Ustawienia → Modele.",
"System prompt": "Prompt systemowy",
"Taken in while it ran:": "Przejęte w trakcie działania:",
"Tasks: %(n)s": "Zadania: %(n)s",
@@ -3014,11 +3082,13 @@ const LEMBAS_I18N = {
"Title": "Tytuł",
"Today": "Dzisiaj",
"Tools": "Narzędzia",
"Tools and skills…": "Narzędzia i umiejętności…",
"Try again": "Spróbuj ponownie",
"Type a command or a chat's title": "Wpisz polecenie lub tytuł czatu",
"Type a folder…": "Wpisz folder…",
"Unpin chat": "Odepnij czat",
"Up to %(n)s at once": "Do %(n)s naraz",
"Upload": "Prześlij",
"Wait until the files are sent.": "Poczekaj, aż pliki zostaną wysłane.",
"Waiting for a free model": "Czeka na wolny model",
"Waiting for subagents: {{running}} running": "Czeka na podagentów: pracuje {{running}}",
@@ -3133,6 +3203,7 @@ const LEMBAS_I18N = {
"Allow": "Povoliť",
"Always": "Vždy",
"Always allow": "Vždy povoliť",
"Always allowed here": "Tu vždy povolené",
"An agent chat works in a folder on %(device)s, through the LLeMbas agent there.": "Agentská konverzácia pracuje v priečinku na zariadení %(device)s cez tamojšieho agenta LLeMbas.",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Agentská konverzácia pracuje v priečinku na tomto počítači cez agenta vstavaného v tejto aplikácii.",
"Another device": "Iné zariadenie",
@@ -3141,6 +3212,7 @@ const LEMBAS_I18N = {
"Approve this code in the page that opened in your browser:": "Potvrďte tento kód na stránke, ktorá sa otvorila vo vašom prehliadači:",
"Archive chat": "Archivovať konverzáciu",
"Archived": "Archivované",
"Ask me about these again": "Znova sa ma na to spýtať",
"Attach files": "Prikladať súbory",
"Auto — asks nothing": "Auto — na nič sa nepýta",
"Automatic compaction is off.": "Automatické zhrnutie je vypnuté.",
@@ -3263,6 +3335,9 @@ const LEMBAS_I18N = {
"Models for scheduled tasks": "Modely pre naplánované úlohy",
"More": "Viac",
"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.",
"Never": "Nikdy",
"New agent chat": "Nová agentská konverzácia",
"New chat": "Nová konverzácia",
@@ -3294,6 +3369,7 @@ const LEMBAS_I18N = {
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Na pozadí nič nebeží. Podagenti, členovia tímu a príkazy na pozadí sa tu zobrazia, kým pracujú.",
"Nothing to report": "Nič na hlásenie",
"Nothing to send: select some output first.": "Nie je čo odoslať: najprv označte časť výstupu.",
"Nothing to switch in this chat.": "V tejto konverzácii nie je čo prepnúť.",
"Nothing yet.": "Zatiaľ nič.",
"Notifications": "Upozornenia",
"One chat that grows": "Jedna konverzácia, ktorá rastie",
@@ -3373,6 +3449,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",
"Skills": "Schopnosti",
"Skipped": "Vynechané",
"Something to remember…": "Niečo na zapamätanie…",
"Source": "Zdroj",
@@ -3389,6 +3466,7 @@ const LEMBAS_I18N = {
"Subagent %(name)s (%(id)s), %(position)s, asks:": "Podagent %(name)s (%(id)s), %(position)s, sa pýta:",
"Subagents": "Podagenti",
"Summarise the earlier turns now and send the summary in their place.": "Zhrnúť skoršie kolá hneď a posielať namiesto nich zhrnutie.",
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Vypnuté len tu. Čo je na začiatku zapnuté, je vaša predvoľba pre tento model v Nastavenia → Modely.",
"System prompt": "Systémový pokyn",
"Taken in while it ran:": "Prevzaté počas behu:",
"Tasks: %(n)s": "Úlohy: %(n)s",
@@ -3454,11 +3532,13 @@ const LEMBAS_I18N = {
"Title": "Názov",
"Today": "Dnes",
"Tools": "Nástroje",
"Tools and skills…": "Nástroje a schopnosti…",
"Try again": "Skúsiť znova",
"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",
"Up to %(n)s at once": "Najviac %(n)s naraz",
"Upload": "Nahrať",
"Wait until the files are sent.": "Počkajte, kým sa súbory odošlú.",
"Waiting for a free model": "Čaká na voľný model",
"Waiting for subagents: {{running}} running": "Čaká na podagentov: pracujú {{running}}",
+3
View File
@@ -109,8 +109,11 @@
<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>
</div>
<p class="status" id="server-status" hidden></p>
<div class="files__tree" id="server-tree"></div>
</section>
</div>
+2
View File
@@ -21,6 +21,7 @@ import { folderName, listed, folderTree, newFolder } from "./list.js"
import { unarchive } from "./archived.js"
import { isProgress, progressLine, progressTicks } from "./progress.js"
import { aimTerminal, terminalButton, terminalOpen, toggleTerminal } from "./terminal.js"
import { scopePopover } from "./scope.js"
const MODES = ["manual", "edit", "auto", "plan"]
@@ -1037,6 +1038,7 @@ export function chatMenu(anchor, chat) {
items.push({ label: chat.pinned ? t("Unpin chat") : t("Pin chat"), run: () => setChat(chat, { pinned: !chat.pinned }) })
items.push({ label: t("Move to folder…"), run: () => moveMenu(anchor, chat) })
}
if (!chat.archived) items.push({ label: t("Tools and skills…"), run: () => scopePopover(anchor, chat) })
if (chat.kind === "agent" && chat.device_session_id) items.push({ label: t("Continue in a terminal…"), run: () => continueInTerminal(chat) })
if (base) items.push({ label: t("Open in the web interface"), run: () => openLink(`${base}/chat/${encodeURIComponent(chat.id)}`) })
items.push(null, chat.archived ? { label: t("Restore chat"), run: () => unarchiveChat(chat) } : { label: t("Archive chat"), run: () => archiveChat(chat) })
+126 -11
View File
@@ -3,7 +3,8 @@
// server's tree, for the chats, read through /api/v2. A file opens in the viewer either way.
import { $, h, clear, icon, ICONS, bytes } from "./dom.js"
import { invoke, get, why } from "./bridge.js"
import { invoke, get, post, del, why } from "./bridge.js"
import { menu, ask } from "./popup.js"
import { openViewer } from "./viewer.js"
// The server's tree as the app's door gives it, read as the person (their own access, no chat):
@@ -18,12 +19,19 @@ export const SERVER_FILES = {
/** A directory in either tree: the server's mounts open like its folders. */
const isDir = (e) => e.kind === "dir" || e.kind === "mount"
/** A lazily opened tree in `box`. `list(path)` -> entries; `open(entry)` for a file. */
function tree(box, { list, open, rootPath }) {
/**
* 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.
*/
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 level = async (parent, path, depth) => {
levels.set(path, { ul: parent, depth })
let entries
try {
entries = await list(path)
@@ -42,26 +50,45 @@ function tree(box, { list, open, rootPath }) {
h("span", { class: "tree__size", text: isDir(e) ? "" : bytes(e.size) }),
)
row.style.setProperty("--depth", String(depth))
const li = h("li", { role: "treeitem" }, row)
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
row.addEventListener("click", async () => {
const opening = row.getAttribute("aria-expanded") !== "true"
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 = h("ul", { role: "group" })
li.append(kids)
await level(kids, e.path, depth + 1)
})
}
row.addEventListener("click", () => expand(row.getAttribute("aria-expanded") !== "true"))
if (opened.has(e.path)) expand(true)
} else row.addEventListener("click", () => open(e))
parent.append(li)
}
}
return level(ul, rootPath, 0)
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)
}
return { done, reload }
}
// --- This computer ------------------------------------------------------------------------------
@@ -99,7 +126,7 @@ function renderLocal() {
head.append(pick)
}
const root = local.root
tree(box, {
return tree(box, {
rootPath: "",
list: async (path) => (await invoke("local_list", { root, path })).entries,
open: async (e) => {
@@ -125,12 +152,96 @@ export async function readServer(path) {
return get(SERVER_FILES.read(path))
}
// The library's writes, as the person (each answers with the directory's listing, or a refusal
// in the server's words):
// POST /files/upload?path= multipart `files` (sent by Rust: `library_upload`, from the dialog)
// POST /files/mkdir { path, name }
// 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
const parentOf = (path) => path.replace(/\/[^/]*$/, "") || "/"
const reloadServer = (dir) => server?.reload(dir === serverRoot ? "/" : dir)
function serverSaid(text, error = false) {
const line = $("server-status")
line.hidden = !text
line.textContent = text || ""
line.className = `status${error ? " is-error" : ""}`
line.setAttribute("role", error ? "alert" : "status")
}
/** Into `dir`: files chosen in the system's dialog. */
export async function uploadInto(dir) {
serverSaid("")
try {
const r = await invoke("library_upload", { dir, title: t("Upload") })
if (r.failed?.length) serverSaid(r.failed.map((f) => `${f.name}: ${f.error}`).join("\n"), true)
if (r.sent) await reloadServer(dir)
} catch (err) {
serverSaid(why(err), true)
}
}
async function makeFolder(dir) {
const name = await ask({ title: t("New folder"), value: "", ok: t("Create"), cancel: t("Cancel") })
if (!name) return
try {
await post("/files/mkdir", { path: dir, name })
serverSaid("")
await reloadServer(dir)
} catch (err) {
serverSaid(why(err), true)
}
}
async function renameEntry(e) {
const name = await ask({ title: t("Rename"), value: e.name, ok: t("Rename"), cancel: t("Cancel") })
if (!name || name === e.name) return
try {
await post("/files/rename", { path: e.path, name })
serverSaid("")
await reloadServer(parentOf(e.path))
} catch (err) {
serverSaid(why(err), true)
}
}
async function deleteEntry(e) {
const yes = await ask({ title: t("Move to the trash"), text: t("Move “%(name)s” to the trash? It can be restored from there.", { name: e.name }), ok: t("Move to the trash"), cancel: t("Cancel"), danger: true })
if (!yes) return
try {
await del(`/files?path=${encodeURIComponent(e.path)}`)
serverSaid(t("Moved to the trash."))
await reloadServer(parentOf(e.path))
} catch (err) {
serverSaid(why(err), true)
}
}
/** What can be done to an entry of the server's tree: a mount (another store) is filled, never
* renamed or deleted from here. A read-only place is refused by the server, in its words. */
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) })
return items
}
export function renderServer() {
const box = $("server-tree")
tree(box, {
serverSaid("")
server = tree(box, {
rootPath: "/",
list: async (path) => (await get(SERVER_FILES.list(path))).entries || [],
list: async (path) => {
const r = await get(SERVER_FILES.list(path))
if (path === "/" && r.path) serverRoot = r.path
return r.entries || []
},
open: (e) => openServerFile(e.path, e.name),
actions: serverActions,
})
}
@@ -177,6 +288,10 @@ export function initExplorer() {
$("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-upload").replaceChildren(icon(ICONS.send))
$("server-upload").addEventListener("click", () => uploadInto(serverRoot))
$("server-mkdir").replaceChildren(icon(ICONS.folder))
$("server-mkdir").addEventListener("click", () => makeFolder(serverRoot))
$("local-refresh").replaceChildren(icon(ICONS.refresh))
$("local-refresh").addEventListener("click", renderLocal)
}
+4 -3
View File
@@ -199,7 +199,8 @@ function escPop(e) {
}
/**
* A small panel under `anchor` (or above it, `up`), holding `content` (an element, or a function
* A small panel under `anchor` (an element, or `{ x, y }` in page pixels as a context menu's
* point; or above it, `up`), holding `content` (an element, or a function
* that draws it and is called again by `redraw`). Escape and a click outside close it. Returns
* { redraw, close }. `align`: "start" (the anchor's left edge) or "end" (its right edge).
*/
@@ -211,7 +212,7 @@ export function popover(anchor, content, { up = false, align = "start", label =
draw()
document.body.append(el)
const place = () => {
const at = anchor.getBoundingClientRect()
const at = anchor instanceof Element ? anchor.getBoundingClientRect() : { left: anchor.x, right: anchor.x, top: anchor.y, bottom: anchor.y }
const r = el.getBoundingClientRect()
const left = align === "end" ? at.right - r.width : at.left
el.style.left = `${Math.max(8, Math.min(left, window.innerWidth - r.width - 8))}px`
@@ -219,7 +220,7 @@ export function popover(anchor, content, { up = false, align = "start", label =
el.style.top = `${Math.max(8, Math.min(top, window.innerHeight - r.height - 8))}px`
}
place()
openPop = { el, back: anchor, onClose }
openPop = { el, back: anchor instanceof Element ? anchor : document.activeElement, onClose }
document.addEventListener("pointerdown", outsidePop, true)
document.addEventListener("keydown", escPop, true)
el.querySelector("button, input, select")?.focus()
+91
View File
@@ -0,0 +1,91 @@
// What a chat may use: the web's chat-settings switches, from a chat's menu. The tool families
// the server offers this chat now (web search among them) and the skills, each on or off for this
// chat only, and the commands it was told to stop asking about.
//
// GET /chats/{chat}/scope -> { scope: { families: [{ gate, label, on }], skills: [{ name, description, on }], allow: [entry] } }
// POST /chats/{chat}/scope { kind: "family"|"skill", name, on } -> { scope }
// POST /chats/{chat}/allow/clear -> { scope }
//
// Narrowing only, on the server: switching on what the model, the permissions or the instance
// left out reaches nothing. Labels, skill names and descriptions are set as text.
import { h } from "./dom.js"
import { get, post, why } from "./bridge.js"
import { popover } from "./popup.js"
/** The switches of `chat`, in a popover under `anchor`. */
export async function scopePopover(anchor, chat) {
const path = `/chats/${encodeURIComponent(chat.id)}`
let scope = null
let error = ""
let busy = false
const pop = popover(anchor, () => draw(), { align: "end", label: t("Tools") })
// Drawn again after each answer: the switch that was used keeps the focus.
const redraw = (key) => {
pop.redraw()
if (key) document.querySelector(`.scope [data-key="${CSS.escape(key)}"]`)?.focus()
}
const send = async (url, body) => {
const key = document.activeElement?.dataset?.key || ""
busy = true
redraw(key)
try {
scope = (await post(url, body))?.scope || scope
error = ""
} catch (err) {
error = why(err)
}
busy = false
redraw(key)
}
const toggle = (kind, name, label, on) =>
h(
"label",
{ class: "check scope__row", title: label.title || null },
h("input", { type: "checkbox", checked: on, disabled: busy, dataset: { key: `${kind}:${name}` }, onchange: (e) => send(`${path}/scope`, { kind, name, on: e.target.checked }) }),
h("span", { text: label.text }),
)
function draw() {
const body = h("div", { class: "pop__body scope" })
if (!scope) {
body.append(error ? h("p", { class: "status is-error", role: "alert", text: error }) : h("p", { class: "muted", text: t("Loading…") }))
return body
}
const families = scope.families || []
const skills = scope.skills || []
const allow = scope.allow || []
if (families.length || skills.length) body.append(h("p", { class: "muted scope__hint", text: t("Switched off here only. What is on to begin with is your default for this model, in Settings → Models.") }))
if (families.length) {
body.append(
h("p", { class: "pop__title", text: t("Tools") }),
h("div", { class: "scope__list" }, families.map((f) => toggle("family", f.gate, { text: f.label || f.gate }, !!f.on))),
)
}
if (skills.length) {
body.append(
h("p", { class: "pop__title", text: t("Skills") }),
h("div", { class: "scope__list" }, skills.map((s) => toggle("skill", s.name, { text: s.name, title: s.description || "" }, !!s.on))),
)
}
if (allow.length) {
body.append(
h("p", { class: "pop__title", text: t("Always allowed here") }),
h("ul", { class: "scope__allow" }, allow.map((a) => h("li", {}, h("code", { text: a })))),
h("div", { class: "pop__actions" }, h("button", { type: "button", class: "btn btn--sm", disabled: busy, text: t("Ask me about these again"), onclick: () => send(`${path}/allow/clear`, {}) })),
)
}
if (!families.length && !skills.length && !allow.length) body.append(h("p", { class: "muted", text: t("Nothing to switch in this chat.") }))
if (error) body.append(h("p", { class: "status is-error", role: "alert", text: error }))
return body
}
try {
scope = (await get(`${path}/scope`))?.scope || { families: [], skills: [], allow: [] }
} catch (err) {
error = why(err)
}
pop.redraw()
}