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': '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',