# Files

> The desktop app's Files and Diffs tabs. A workspace tree beside the open files, with an editor, the working-tree diff, search, find and diagnostics, and the session's edits and the working tree as reviewable diffs, on a FilesHost.

`zidane-ui/files` holds the desktop app's Files and Diffs tabs. `FilesTab` shows a folder's tree beside its open files. A file opens in the viewer, or in the editor when the host can write, and a changed file opens on its working-tree diff. The tab searches file names and contents, finds in the file on screen, and shows project-tool and language-server findings. `DiffsTab` shows what changed: the session's edits and the working tree against HEAD, a file tree beside virtualised diffs you can comment on. A `FilesHost` provides the files, the git state and the editor preferences. The previews below run on a fixture host.

## Mount it

```tsx
import { createServerClient, createServerFilesHost } from 'zidane-ui/adapters'
import { FilesProvider, FilesTab } from 'zidane-ui/files'

const client = createServerClient({ url, token })
const files = createServerFilesHost({ client, sessionOf: cwd => openSessionFor(cwd), storage: localStorage })

export function FilesPanel({ session }) {
  return (
    <FilesProvider host={files}>
      <FilesTab scope={{ root: session.cwd, cwd: session.cwd, session: session.id }} />
    </FilesProvider>
  )
}
```

`AppHostProvider` (`zidane-ui/app`) puts `host.files` in a `FilesProvider` for you. The tabs fill their parent (`flex-1 min-h-0`): give them a sized frame, like a side panel's slot.

## What the app plugs in

The tabs read data from the host, and take what only the app knows as props:

| Prop | Gives |
| - | - |
| `scope` | The folder (`cwd`, or `workspace` when the session's files are elsewhere) and the session it is bound to |
| `layout` | Where the open tabs, the active file, the tree toggles and its width live (`FilesLayoutStore`). Default: in memory, per mount. `createMemoryFilesLayout(initial)` makes one. |
| `chat` | Where "Add to chat" and review comments go (`FilesChatTarget`: `mention(path)`, `attach(name, text)`). Without one they show disabled. |
| `useEdits` | A hook giving the bound session's edit events: changed files in the tree, the Diffs tab's Session source |
| `useKeyScope` | A hook registering next, previous, close and find on the tab's root, for the app's key routing |
| `revealRequest`, `onRevealTaken`, `takeRevealPosition` | A path handed over from elsewhere: a file opens at a position, a folder is revealed in the tree |
| `onOpenExternal` | ⌘/Ctrl-click on a tree row opens the path outside the panel |
| `renderTabGlyph` | A file tab's glyph (default: its icon) |
| `readOnly` | Nothing changes: no saves, no file actions (default `false`) |
| `fileActions` | The tree's new file and folder, rename, move, copy, delete and drops, when the host has `entries` (default `true`) |
| `workingTreeRequest`, `onWorkingTreeTaken` | `DiffsTab`: open on the working tree |
| `createParseWorker` | `DiffsTab`: the worker that parses working-tree patches (`zidane-ui/code/editor/patch-parse.worker`). Default: this thread. |

## Pick a host

| Host | From | Offers |
| - | - | - |
| `createServerFilesHost({ client, sessionOf, storage })` | `zidane-ui/adapters` | Listing, reading and writing through a session's workspace (`sessions.workspaceListFiles`, `workspaceReadFile`, `workspaceWriteFile`, `workspaceFileSize`), its empty folders (`sessions.workspaceEmptyDirs`), the file actions (`sessions.workspaceCreate`, `workspaceMove`, `workspaceCopy`, `workspaceRemove`, an `admin` token; removal is for good; a drop is sent one file per call), folder changes from the session's tool results, editor preferences in `storage`. A write re-reads the file and refuses when its text moved. In a folder of the server's workspace roots: content search (`files.search`), git status and the working-tree diff (`git.fileStatus`, `git.diff`), project diagnostics (`files.diagnostics`, an `admin` token; `files.diagnosticsInvalidate` on a rerun, a refresh or a tool result, so the next run checks again), each offered when the connection may call it (build the host after the handshake). Media files play from the server's `GET /zidane/media` (`mediaUrl`, under a ticket the host registers on each connection: `files.mediaTicket`), relative to the page's origin: the web app the server serves. No language servers (they run per window, where the editor is). |
| `createFixtureFilesHost(options)` | `zidane-ui/files/testing` | A folder in memory, its git status and patches, for demos and tests |
| Your own | `zidane-ui/files/headless` | Any subset of the members below |

The server host does not read the handshake: every member is there whatever the token may call. Gate it yourself, and leave `files` out of the `AppHost` unless `client.can('sessions.workspaceListFiles')`. Built without that check, a server or token without the workspace methods shows a Files tab whose every listing fails. [Build your own remote app](https://zidane.si/docs/web-app.md#build-your-own-remote-app) shows it with the other hosts.

| Member | Gives |
| - | - |
| `list(cwd, { showIgnored })` | The tree |
| `read(cwd, path)` | Open files (`null`: gone) |
| `write(cwd, path, contents, expectedMtimeMs)` | Editing: ⌘S saves, refused with `{ ok: false, reason: 'changed' }` when the file moved, which raises the conflict bar. Without it, files are read-only |
| `entries` | The tree's file actions: New file and New folder (named in place), rename (F2), move (drag a row onto a folder; ⌥ copies), Cut / Copy / Paste (⌘X / ⌘C / ⌘V), Duplicate, Delete (⌘⌫, asked first: `removal(cwd)` says whether it goes to the trash or is gone for good), and files dropped from the file manager onto a folder or the editor (`importFiles`). Nothing is overwritten: a taken name is refused, a copy takes the next free name (`a copy.ts`). The open tabs follow a rename or a move |
| `search(cwd, query, options)` | Search in file contents (without it, names only) |
| `gitStatus(cwd)` | Changed marks, "Changed only", diff mode for changed files |
| `workingTreeDiff(cwd, { path, context })` | The working-tree diffs: a changed file's diff, the Diffs tab's Working source |
| `watch(scope, onChange)` | Refreshes when the folder may have changed (a tool result, the repository moving) |
| `editorPrefs` | Editing, saving, indentation, wrapping, diagnostics and language-server preferences (`createStorageEditorPrefs`; `useEditorPrefs()` reads them) |
| `diagnostics` | Project-tool findings, the findings chip, counts in the Diffs file tree |
| `lsp` | Live findings while typing, hovers, completion, go to definition |
| `system` | "Open in editor" and "Reveal in Finder" for local folders |
| `mediaUrl(source)` | Images, video, audio and PDFs shown as themselves, before and after in Diffs |

## Test your host

```ts
import { describeFilesHostContract } from 'zidane-ui/files/testing/contract'

describeFilesHostContract('my host', backend => makeHarness(backend), ['list', 'read', 'write'])
```

| Entry point | Holds |
| - | - |
| `zidane-ui/files` | `FilesProvider`, `useFilesHost`, `useEditorPrefs`, `useFilesLabels`, `FilesTab`, `DiffsTab`, the code attachment names (`attachmentName`, `parseCodeAttachmentName`, `fenceFor`, `codeAttachmentLabel`: an `attachmentLabel` renderer slot that names a code or terminal excerpt by its source and range), terminal selections as attachments (`serializeTerminalExcerpt`, `terminalRangeLabel`, `terminalRangeDetail`) |
| `zidane-ui/files/headless` | `FilesHost` and its types, the editor preferences (`parseEditor`, `DEFAULT_EDITOR`, `createStorageEditorPrefs`), the view state (`FilesScope`, `filesRootOf`, `FilesLayout`, `createMemoryFilesLayout`, `FilesChatTarget`), `matchRanges`, `mediaTypeOfPath`, the entry helpers (`droppedItems`, `droppedImportItems`, `movedPath`, `topLevelPaths`, `entryRefusalText`), the labels |
| `zidane-ui/files/testing` | `createFixtureFilesHost`, `memoryEntryPort` (an `EntryPort` over memory, for a fake backend), `DEMO_FILES` |
| `zidane-ui/files/testing/contract` | `describeFilesHostContract` |
