# Machines

> Which agent host or server the app talks to and how it is, the notices a session shows when its server or machine is away, the Hosts page and a server's settings (its card, auto-update, machines, tokens, settings sync), and the desktop app's daemon menu.

`zidane-ui/machines` shows where an app's sessions run and how that place is: This computer, the saved zidane-servers, or the one server that serves a web app. A `MachinesHost` holds the hosts and their connection, the machines a server holds, this computer lent to a server, a server's self-update and client tokens, and the desktop app's local daemon. The preview below runs on a fixture host.

## Mount it

A web app served by zidane-server builds the host over the client it shares with the chat:

```tsx
import { createServerClient, createServerMachinesHost } from 'zidane-ui/adapters'
import { MachinesProvider, ServerCard, ServerUpdatePanel, TokensPanel } from 'zidane-ui/machines'
import { useReadable } from 'zidane-ui/chat/headless'

const client = createServerClient({ url: 'wss://zidane.example.com', token })
const machines = createServerMachinesHost({ client })

function ServerSettings() {
  const [server] = useReadable(machines.hosts!).hosts
  return (
    <>
      <ServerCard host={server} />
      <ServerUpdatePanel host={server} />
      <TokensPanel host={server} />
    </>
  )
}

export function Root() {
  return (
    <MachinesProvider host={machines}>
      <ServerSettings />
    </MachinesProvider>
  )
}
```

`AppHostProvider` (`zidane-ui/app`) puts `host.machines` in a `MachinesProvider` for you. Outside a provider the components read an empty host: what needs a member hides. `confirm` on the provider (or on a component) asks before a destructive action on your modal layer; without it the provider asks with the design system's `ConfirmDialog`.

## The Hosts page and settings sync

`HostsSection` is Settings › Accounts › Hosts: This computer and the saved servers, each row with its status, what it can do and its last sync; a host's page has its card, Auto-update, Capabilities, Settings sync and Machines. `hostsSettingsSection(props)` hands it to `SettingsShell` with the layout's title, icon and search entries. The preview below runs on a fixture host with sync on: open Box.

```tsx
import { SettingsShell } from 'zidane-ui/settings'
import { hostsSettingsSection } from 'zidane-ui/machines'

<SettingsShell sections={[...sections, hostsSettingsSection({ confirm })]} />
```

Settings sync pushes this computer's settings to a server, one way (`docs/SERVER.md` § Settings sync). `HostSyncPanel` sets the mode and the categories, opts each API key in after a confirmation (`SyncAccounts`, with "Sign in there" for a provider that signs in with OAuth: `HostSignIn` over the host's `signIn`), takes over from another device, resets the server's overrides and shows the last push (`SyncOutcomeBadge`). In the server's own Settings, `SyncMarker` under a row says which device the value came from, or that it was edited there since; `useServerSyncStatus(hostId)` reads the record once for a page of markers.

| Host | `sync` |
| - | - |
| The desktop app's | Everything: this computer is the device that pushes (`state`, `configure`, `push`, `takeOver`), plus the server's record, `reset` and `providers` |
| `createServerMachinesHost` | The server's side: `status` (`sync.status`), `reset` (`sync.reset`), `providers` (`auth.status`), and `signIn` (`auth.oauth.*`). A browser is not a syncing device, so there is no Sync section. `sync.*` needs an admin token: with a `chat` token the section shows greyed out, *Admin only*. |

## What a token may do

zidane-server answers by scope. `createServerMachinesHost` reads what the handshake allows (`client.can`) into `permitted`, and the components follow it:

| Scope | Sees |
| - | - |
| `chat` | The server's card (name, version, uptime, *Chat access*, *Get admin*), its machines, the auto-update state |
| `admin` | Also Check now, Install now (a release that is ready or waiting), Turn on / off, forgetting a machine, Add a machine, the token list with create and revoke, and Install on a missing tool |

Under a `chat` token, the parts that need `admin` show greyed out and inert, with an *Admin only* tooltip (`Restricted`): the token form, the update controls, Add a machine, Install. The update panel still shows its state. Until the handshake, `permitted` is `null` and these panels wait.

## Parts

| Part | Does |
| - | - |
| `ServerOfflineBanner` | Replaces the composer while the session's server can't be reached, says why (refused token, another server at the address, protocol version, still connecting, removed) and offers Retry when the host can |
| `HostRemovedBanner`, `ComposerNotice` | The open chat closed because its server was removed; why a new chat could not be created. Both dismiss. |
| `MachineWaitingBanner` | "Waiting for laptop…" while a run is paused on an offline machine (`machine` from the session's notices, `machineWaiting` in `zidane/chat/pure`), gone once the host hears it is back |
| `ServerCard` | A server's name, a status dot (with a chip while it is not connected), one line with its URL (copies on click) and facts, a Test button, and a ⋯ menu with Rename, Change URL and Remove where the host offers them. `badge` puts your own chip after the status. A chip says *Chat access* or *Admin access*. With a `chat` token, *Get admin* shows the server's own command from the hello (`adminCommands`): with `addHost`, the token command and a field that saves the pasted token in place of the old one; without it (the web app's own server), the pairing command. |
| `AddServerForm` | URL, a masked device token and an optional name; Test checks without saving, Add checks and saves. A refusal says what to do, per registry error code. |
| `ServerUpdatePanel` | The server's self-update state and actions, read again every few seconds while it works |
| `ServerMachinesPanel` | Share this computer with the server (folders from `pickFolder`, a machine token when the server cannot mint one), and use the server as a machine. It lists the server's machines, with forget. Each machine shows online or offline since when, its version (with a warning when it differs from the server's), its GPUs and folders. Add a machine asks for an optional machine name (checked with `isValidMachineName`, empty sends none), then shows the join command from `machines.invite` |
| `TokensPanel` | The server's client tokens with their scope and dates; create one (its secret shows once) and revoke |
| `DaemonMenu` | The desktop app's daemon dot and its menu: status, the sessions the daemon holds, reconnect, restart, relaunch, daemon mode, diagnostics. Nothing without `daemon`. |
| `CapabilitiesView`, `CapabilityTags` | What a host can do (OS, free disk, tools on PATH with their version, the OS sandbox, environment kinds, what it allows) with Re-check, and a few tags for its row. A missing tool offers Install when the host can run its recipe without root (`installTool`, admin only), a command to copy when it needs root, or a link to its install page. `useHostCapabilities(hostId, enabled)` reads the host's `capabilities` |
| `CopyText` | Text that copies itself on click |
| `HostsSection`, `hostsSettingsSection` | The Hosts page: the list (dates from `savedHosts`), the add form, a host's page. `confirm` asks on your modal layer. |
| `HostSyncPanel`, `useHostSync(hostId)` | A server's settings sync from this computer. Nothing without `sync.state`. |
| `SyncAccounts` | This computer's API keys and sign-ins against the server's providers |
| `SyncOutcomeBadge` | "Sync complete" or "Partial sync", with what failed on hover or focus |
| `SyncMarker`, `useServerSyncStatus(hostId)` | Where a server's setting came from; "Sync again" with `sync.reset` |
| `HostSignIn` | A browser sign-in whose tokens stay on a server (a provider, an MCP server of its catalog) |

Hooks: `useMachinesHost`, `useMachinesLabels`, `useMachinesConfirm`, `usePermitted(action)`, `useOnlineMachines()`, `usePresenceMap()` and `useSessionPresence(id)` (each session's attached windows, run and whether it waits on you, from the host's `presence`; one session's hook re-renders only when that session changes), and `useLentServers(enabled)`, the machine names of the servers this computer uses as machines, so a picker shows them once, as servers (`hiddenMachines` on `WherePicker`). `escapeCancels`, `HOST_CARD_ICON_CLASS` and the field limits (`HOST_NAME_MAX`, `HOST_URL_MAX`, `HOST_TOKEN_MAX`) are there for an app's own host pages.

## Pick a host

| Host | From | Talks to |
| - | - | - |
| `createServerMachinesHost({ client })` | `zidane-ui/adapters` | zidane-server: the server itself, `machines.list` / `machines.forget` and the `machine:status` signal, `server.update.*`, `tokens.list` / `create` / `revoke` and the `tokens:changed` signal, `sync.status` / `sync.reset` and the `sync:changed` signal, `auth.status`, `auth.oauth.*` |
| `createFixtureMachinesHost(options)` | `zidane-ui/machines/testing` | Memory, for demos and tests |
| The desktop app's | `gui/src/renderer/src/hosts/electron-machines-host.ts` | IPC: the saved-server registry and its dates, machine links, the folder dialog, the local daemon, settings sync and a sign-in on a server |
| Your own | `zidane-ui/machines/headless` | Anything. `lazyReadable`, `createMachineLists` and `createOnlineMachines` are the stores every host builds. |

| Member | Gives |
| - | - |
| `hosts`, `refreshHosts()` | The hosts and their connection (`HostsState`: `enabled`, then This computer and the servers) |
| `retry(id)` | Reconnect now. zidane-server's client reconnects by itself, so the server host leaves it out. |
| `testHost`, `addHost`, `renameHost`, `setHostUrl`, `removeHost` | The saved-server registry. All but `removeHost` answer as data (`HostCheckResult`). |
| `savedHosts` | The registry as last read, with the dates the live statuses lack (`addedAt`, `lastConnectedAt`), and `refresh()` |
| `sync` | `state(hostId)`, `configure`, `push`, `takeOver` (this computer's sync), `status(hostId)` and `reset` (the server's record), `providers(hostId)`. Each read is a `HostSyncRead`: `state` kept current while subscribed, `refresh()`. `createHostSyncReads` builds them. |
| `signIn(hostId, target, onDone)` | A `SignInFlow` for a provider or an MCP server on that server |
| `machines` | `list(hostId)` (a `MachineList`, kept current while read), `online` (names last heard online), `forget`, and `invite(hostId, { name?, ttlMs? })`, a `MachineInvite` (`code`, `expiresAt`, `join`) over `tokens.machineInvite`. `createServerMachinesHost` offers `invite` only with its `url` option, the server's address as a machine reaches it |
| `links`, `pickFolder()` | This computer shared with servers and servers used as machines; the folder dialog |
| `updates` | `status`, `check`, `install`, `configure` |
| `tokens` | `list`, `create`, `revoke`, `onChanged` (`TokensPanel` reads the list again) |
| `daemon` | `health`, `status()`, `reconnect`, `restart`, `stopRun`, `relaunchApp` |
| `permitted` | What the members may do now (`MachinesAction`); absent: everything they offer |

A failed action rejects with a `MachinesError` (`kind`, `detail`; `sync` for a sync action); `machinesError(kind, error)` makes one from anything a backend threw, and the `machinesError` label words it.

The wording helpers take the labels: `daemonStatusText`, `hostCheckMessage`, `hostCheckOkLine`, `hostStatusText`, `hostStatusTone`, `updateStateLine`, `UPDATE_BUSY_STATES`, `linkStateText`, `agoText`, `uptimeText`, `isPermitted`, `isMachinesError`, and for sync `syncOutcome`, `syncBlockedReason`, `shortList`. Every string is a `machines…` label (`DEFAULT_MACHINES_LABELS`).

## Test your host

```ts
import { describeMachinesHostContract } from 'zidane-ui/machines/testing/contract'

describeMachinesHostContract('my host', backend => makeHarness(backend), ['hosts', 'machines', 'updates'])
```

`describeMachinesSyncContract(name, makeHarness, capabilities)`, from the same entry, runs `sync` and `signIn` the same way over a scripted sync backend.

| Entry point | Holds |
| - | - |
| `zidane-ui/machines` | `MachinesProvider` and its hooks, the banners, `DaemonMenu`, `ServerCard`, `AddServerForm`, `ServerUpdatePanel`, `ServerMachinesPanel`, `TokensPanel`, `HostsSection`, `HostSyncPanel`, `SyncAccounts`, `SyncOutcomeBadge`, `SyncMarker`, `HostSignIn`, `hostsSettingsSection` |
| `zidane-ui/machines/headless` | `MachinesHost` and its types, the stores (`createPresenceStore` builds a host's `presence` over a feed of snapshots, `createHostSyncReads` its sync reads), the wording helpers, the labels |
| `zidane-ui/machines/testing` | `createFixtureMachinesHost`, the demo hosts, machines, tokens, daemon, sync state (`DEMO_SYNC`) and server providers |
| `zidane-ui/machines/testing/contract` | `describeMachinesHostContract`, `describeMachinesSyncContract` |
