feat(files): a Files section over the caller's whole space
Nightly Build / build (push) Successful in 5m36s

Until now file browsing existed only inside a project, and the two memory
stores were reachable only by the agent's tools. `#files` is the general
surface: home, both memory stores, the shared folders and projects the
caller belongs to, plus the read-only skills and docs trees.

The root is virtual, and that is the design. Anchoring at `~` is wrong:
the explorer reads host-side, while `shared/`, `projects/`, `skills/` and
`docs/` are bind mounts inside the container — a page rooted at the home
would show less than the user has with no way to reach the rest, and on
native Linux would show Docker's empty mountpoint stubs, a door that
appears to work and leads nowhere. So level 0 is a synthetic list from the
new `GET /api/files/roots`, serialized from the caller's `UserFs` plus the
two virtual memory roots. It sends `kind`, never a label: labels are copy
and get translated.

`GET /api/files/dir` now answers `{ path, can_write, entries }`, and a
memory path is classified before `resolve_view_path` (which refuses one)
and listed from `memory_docs`: one level derived from the flat key space
by the pure `memory_docs::immediate_children`, over a single query whose
unslashed prefix also spots an exact note as "not a directory". Memory is
read-only from the page — every writer routes through `resolve_view_path`,
and `shared-memory/*` is `@fs_write require` for the agent, so a button
that walks past that rule is a decision of its own.

The explorer moves out of projects into `shared/file-explorer.js`, taking
`root` + `rootLabel` and reading `can_write` from the listing rather than
from its host: writability changes per branch and comes from the same
`UserFs::can_write_to` the server rejects writes with, so the buttons
offered and the writes accepted cannot disagree. Deep-linking needed it
steerable without a two-way binding, hence `rel` in and
`explorer-navigate` out — the event fires only for a click, never for a
`rel` the host set, so echoing it back is a no-op.

The URL carries the agent path of the open folder in one parameter, the
same vocabulary the assistant uses, so a link is shareable and pasteable
into a conversation; which root it belongs to is derived, not stored.

docs/: a new files.md, plus two pages this made false — shared-folders.md
claimed in three places that a shared folder has no explorer, and
memory.md never said a user can now read their own notes.
This commit is contained in:
Daniele
2026-08-22 20:09:56 +01:00
parent 934726a75d
commit 488c702517
19 changed files with 870 additions and 148 deletions
+36 -20
View File
@@ -8,6 +8,7 @@ export default {
'nav.inbox': 'Inbox',
'nav.dashboard': 'Dashboard',
'nav.projects': 'Projects',
'nav.files': 'Files',
'nav.tasks': 'Task Manager',
'nav.tasks.running': 'Running Tasks',
'nav.tasks.cron': 'Cron Jobs',
@@ -266,26 +267,40 @@ export default {
'projects.share.access.readwrite':'Read & write',
'projects.tabs.files': 'Files',
'projects.tabs.sharing': 'Sharing',
'projects.files.col.name': 'Name',
'projects.files.col.created': 'Created',
'projects.files.col.modified': 'Modified',
'projects.files.col.size': 'Size',
'projects.files.empty': 'This folder is empty.',
'projects.files.refresh': 'Refresh',
'projects.files.drop': 'Drop files here to upload',
'projects.files.btn.new_folder': 'New folder',
'projects.files.btn.upload': 'Upload',
'projects.files.btn.download': 'Download ZIP',
'projects.files.uploading': 'Uploading…',
'projects.files.action.download': 'Download',
'projects.files.action.rename': 'Rename',
'projects.files.action.delete': 'Delete',
'projects.files.confirm.delete_file': 'Delete "{name}"?',
'projects.files.confirm.delete_dir': 'Delete the folder "{name}" and everything inside it?',
'projects.files.modal.mkdir': 'New folder',
'projects.files.modal.rename': 'Rename "{name}"',
'projects.files.modal.name': 'Name',
'projects.files.error.name': 'Enter a valid name (no slashes).',
// ── File explorer (shared component: projects, Files section) ───────────────
'files.col.name': 'Name',
'files.col.created': 'Created',
'files.col.modified': 'Modified',
'files.col.size': 'Size',
'files.empty': 'This folder is empty.',
'files.refresh': 'Refresh',
'files.drop': 'Drop files here to upload',
'files.btn.new_folder': 'New folder',
'files.btn.upload': 'Upload',
'files.btn.download': 'Download ZIP',
'files.uploading': 'Uploading…',
'files.action.download': 'Download',
'files.action.rename': 'Rename',
'files.action.delete': 'Delete',
'files.confirm.delete_file': 'Delete "{name}"?',
'files.confirm.delete_dir': 'Delete the folder "{name}" and everything inside it?',
'files.modal.mkdir': 'New folder',
'files.modal.rename': 'Rename "{name}"',
'files.modal.name': 'Name',
'files.error.name': 'Enter a valid name (no slashes).',
'files.title': 'Files',
'files.note.roots': 'Everywhere you can go: your home, your two memory stores, and the folders shared with you. These are the same paths the assistant uses.',
'files.back': 'All files',
'files.badge.readonly': 'Read-only',
'files.error.unknown_path': '"{path}" is not one of your folders.',
'files.root.home': 'Home',
'files.root.user_memory': 'Personal memory',
'files.root.shared_memory': 'Shared memory',
'files.root.shared': 'Shared folder',
'files.root.project': 'Project',
'files.root.skills': 'Skills',
'files.root.docs': 'Documentation',
// ── Project detail ──────────────────────────────────────────────────────────
'project_board.back': 'Projects',
@@ -1226,6 +1241,7 @@ export default {
'common.cancel': 'Cancel',
'common.loading': 'Loading…',
'common.saved': 'Saved',
'common.create': 'Create',
// ── Shared Folders (blueprint §6) ────────────────────────────────────────────
'nav.shared_folders': 'Shared Folders',
+36 -20
View File
@@ -8,6 +8,7 @@ export default {
'nav.inbox': 'Boîte de réception',
'nav.dashboard': 'Tableau de bord',
'nav.projects': 'Projets',
'nav.files': 'Fichiers',
'nav.tasks': 'Gestionnaire de tâches',
'nav.tasks.running': 'Tâches en cours',
'nav.tasks.cron': 'Tâches Cron',
@@ -266,26 +267,40 @@ export default {
'projects.share.access.readwrite':'Lecture et écriture',
'projects.tabs.files': 'Fichiers',
'projects.tabs.sharing': 'Partage',
'projects.files.col.name': 'Nom',
'projects.files.col.created': 'Création',
'projects.files.col.modified': 'Modification',
'projects.files.col.size': 'Taille',
'projects.files.empty': 'Ce dossier est vide.',
'projects.files.refresh': 'Actualiser',
'projects.files.drop': 'Déposez les fichiers ici pour les envoyer',
'projects.files.btn.new_folder': 'Nouveau dossier',
'projects.files.btn.upload': 'Envoyer',
'projects.files.btn.download': 'Télécharger (ZIP)',
'projects.files.uploading': 'Envoi…',
'projects.files.action.download': 'Télécharger',
'projects.files.action.rename': 'Renommer',
'projects.files.action.delete': 'Supprimer',
'projects.files.confirm.delete_file': 'Supprimer « {name} » ?',
'projects.files.confirm.delete_dir': 'Supprimer le dossier « {name} » et tout son contenu ?',
'projects.files.modal.mkdir': 'Nouveau dossier',
'projects.files.modal.rename': 'Renommer « {name} »',
'projects.files.modal.name': 'Nom',
'projects.files.error.name': 'Saisissez un nom valide (sans barres obliques).',
// ── File explorer (shared component: projects, Files section) ───────────────
'files.col.name': 'Nom',
'files.col.created': 'Création',
'files.col.modified': 'Modification',
'files.col.size': 'Taille',
'files.empty': 'Ce dossier est vide.',
'files.refresh': 'Actualiser',
'files.drop': 'Déposez les fichiers ici pour les envoyer',
'files.btn.new_folder': 'Nouveau dossier',
'files.btn.upload': 'Envoyer',
'files.btn.download': 'Télécharger (ZIP)',
'files.uploading': 'Envoi…',
'files.action.download': 'Télécharger',
'files.action.rename': 'Renommer',
'files.action.delete': 'Supprimer',
'files.confirm.delete_file': 'Supprimer « {name} » ?',
'files.confirm.delete_dir': 'Supprimer le dossier « {name} » et tout son contenu ?',
'files.modal.mkdir': 'Nouveau dossier',
'files.modal.rename': 'Renommer « {name} »',
'files.modal.name': 'Nom',
'files.error.name': 'Saisissez un nom valide (sans barres obliques).',
'files.title': 'Fichiers',
'files.note.roots': 'Tout ce que vous pouvez atteindre : votre dossier personnel, vos deux mémoires et les dossiers partagés avec vous. Ce sont les chemins qu\'utilise l\'assistant.',
'files.back': 'Tous les fichiers',
'files.badge.readonly': 'Lecture seule',
'files.error.unknown_path': '« {path} » ne fait pas partie de vos dossiers.',
'files.root.home': 'Dossier personnel',
'files.root.user_memory': 'Mémoire personnelle',
'files.root.shared_memory': 'Mémoire partagée',
'files.root.shared': 'Dossier partagé',
'files.root.project': 'Projet',
'files.root.skills': 'Compétences',
'files.root.docs': 'Documentation',
// ── Détail du projet ────────────────────────────────────────────────────────
'project_board.back': 'Projets',
@@ -1213,6 +1228,7 @@ export default {
'common.cancel': 'Annuler',
'common.loading': 'Chargement…',
'common.saved': 'Enregistré',
'common.create': 'Créer',
// ── Shared Folders (blueprint §6) ────────────────────────────────────────────
'nav.shared_folders': 'Dossiers partagés',
+36 -20
View File
@@ -8,6 +8,7 @@ export default {
'nav.inbox': 'Richieste',
'nav.dashboard': 'Dashboard',
'nav.projects': 'Progetti',
'nav.files': 'File',
'nav.tasks': 'Gestione attività',
'nav.tasks.running': 'Attività in corso',
'nav.tasks.cron': 'Attività ricorrenti',
@@ -290,26 +291,40 @@ export default {
'projects.share.access.readwrite':'Lettura e scrittura',
'projects.tabs.files': 'File',
'projects.tabs.sharing': 'Condivisione',
'projects.files.col.name': 'Nome',
'projects.files.col.created': 'Creazione',
'projects.files.col.modified': 'Ultima modifica',
'projects.files.col.size': 'Dimensione',
'projects.files.empty': 'Questa cartella è vuota.',
'projects.files.refresh': 'Aggiorna',
'projects.files.drop': 'Trascina qui i file per caricarli',
'projects.files.btn.new_folder': 'Nuova cartella',
'projects.files.btn.upload': 'Carica',
'projects.files.btn.download': 'Scarica ZIP',
'projects.files.uploading': 'Caricamento…',
'projects.files.action.download': 'Scarica',
'projects.files.action.rename': 'Rinomina',
'projects.files.action.delete': 'Elimina',
'projects.files.confirm.delete_file': 'Eliminare "{name}"?',
'projects.files.confirm.delete_dir': 'Eliminare la cartella "{name}" e tutto il suo contenuto?',
'projects.files.modal.mkdir': 'Nuova cartella',
'projects.files.modal.rename': 'Rinomina "{name}"',
'projects.files.modal.name': 'Nome',
'projects.files.error.name': 'Inserisci un nome valido (senza barre).',
// ── File explorer (shared component: projects, Files section) ───────────────
'files.col.name': 'Nome',
'files.col.created': 'Creazione',
'files.col.modified': 'Ultima modifica',
'files.col.size': 'Dimensione',
'files.empty': 'Questa cartella è vuota.',
'files.refresh': 'Aggiorna',
'files.drop': 'Trascina qui i file per caricarli',
'files.btn.new_folder': 'Nuova cartella',
'files.btn.upload': 'Carica',
'files.btn.download': 'Scarica ZIP',
'files.uploading': 'Caricamento…',
'files.action.download': 'Scarica',
'files.action.rename': 'Rinomina',
'files.action.delete': 'Elimina',
'files.confirm.delete_file': 'Eliminare "{name}"?',
'files.confirm.delete_dir': 'Eliminare la cartella "{name}" e tutto il suo contenuto?',
'files.modal.mkdir': 'Nuova cartella',
'files.modal.rename': 'Rinomina "{name}"',
'files.modal.name': 'Nome',
'files.error.name': 'Inserisci un nome valido (senza barre).',
'files.title': 'File',
'files.note.roots': 'Tutto quello che puoi raggiungere: la tua home, le tue due memorie e le cartelle condivise con te. Sono gli stessi percorsi che usa l\'assistente.',
'files.back': 'Tutti i file',
'files.badge.readonly': 'Sola lettura',
'files.error.unknown_path': '"{path}" non è una delle tue cartelle.',
'files.root.home': 'Home',
'files.root.user_memory': 'Memoria personale',
'files.root.shared_memory': 'Memoria condivisa',
'files.root.shared': 'Cartella condivisa',
'files.root.project': 'Progetto',
'files.root.skills': 'Skill',
'files.root.docs': 'Documentazione',
// ── Dettaglio progetto ──────────────────────────────────────────────────────
'project_board.back': 'Progetti',
@@ -1213,6 +1228,7 @@ export default {
'common.cancel': 'Annulla',
'common.loading': 'Caricamento…',
'common.saved': 'Salvato',
'common.create': 'Crea',
// ── Cartelle condivise (blueprint §6) ────────────────────────────────────────
'nav.shared_folders': 'Cartelle condivise',