# Embedded terminals

> Real terminals in your app over a TerminalHost. A drawer under the chat, a side-panel tab, and a view that attaches to a terminal, replays its history and streams its output.

`zidane-ui/terminal` draws terminals with `@tahul/ghostty-web`, which loads on first use. Two layers build on it:

- `TerminalSurface` is a canvas over any process. A `TerminalAdapter` carries the output and the keys.
- The terminal area sits over a `TerminalHost`. A store holds each scope's terminals. `TerminalPanel` shows a tab strip and the active terminal. `TerminalDrawer` puts that panel under the chat, `TerminalToggle` shows or hides the drawer from the header, and `TerminalPanelTab` is the side panel's Terminal tab.

A scope is whose terminals they are: a session's (they run in its environment), a project folder's, or a new-chat draft's. The desktop app runs them in its daemon. The demo below runs on the fixture host: its shells echo what you type.

## Mount it

```tsx
import { createServerClient, createServerTerminalHost } from 'zidane-ui/adapters'
import { TerminalDrawer, TerminalProvider, TerminalToggle } from 'zidane-ui/terminal'

const client = createServerClient({ url, token })
const terminals = createServerTerminalHost({ client })

export function Chat({ sessionId, cwd }: { sessionId: string, cwd: string }) {
  const scope = { root: cwd, cwd, session: sessionId }
  return (
    <TerminalProvider host={terminals} actions={{ openUrl: (_scope, url) => window.open(url, '_blank') }}>
      <header><TerminalToggle scope={scope} /></header>
      <Conversation />
      <TerminalDrawer scope={scope} />
    </TerminalProvider>
  )
}
```

`AppHostProvider` (`zidane-ui/app`) puts `host.terminal` in a `TerminalProvider` for you. If code outside React also drives terminals (a keybinding that opens the drawer, a draft handed to its session), create the store yourself with `createTerminalStore(host)` and pass it as `store`. Then `store.toggleDrawer(scope)`, `store.openInDrawer(scope)`, `store.revealTerminal(scope, id)` and `store.adoptDraft(draft, session)` work from anywhere.

## States

The panel says why there are no terminals. That can be the host's reason, a link that is still coming up (`waiting`), or a shell that failed to start. The header toggle shows a dot when the agent starts a terminal while nothing shows the session's terminals.

## What the app does

`TerminalProvider`'s `actions` hold what only your app can do. Leave one out and the control that needs it is hidden.

| Action | Gives |
| - | - |
| `openUrl(scope, url, external, cwd)` | Where web links open. Default: a new window. |
| `openFile(scope, path, event, at)` | Where file paths open (`src/app.ts:12:5`). Without it, paths don't link. |
| `addToChat(scope, excerpt)`, `canAddToChat(scope)` | The right-click menu's Add to chat. It gets the selection with its line range. |
| `openSettings()` | The unavailable state's Open Settings button |
| `remoteName(scope)` | The machine a scope's terminals run on when it isn't this one. Its loopback links are named and never opened. |
| `registerTabScope(element, handlers)`, `drawerProps` | Your keyboard model: step between terminals, close one, mark the drawer as a focus region |

`enabled={false}` removes the drawer and the toggle. `symbolsFontUrl` serves the Nerd Font symbols face (`SYMBOLS_FONT`) before a terminal measures its cells.

## Pick a host

| Host | From | Runs |
| - | - | - |
| `createServerTerminalHost({ client })` | `zidane-ui/adapters` | zidane-server's `terminal.*` methods (an `admin` token). It has project and draft terminals when the server offers them (`projectTerminals`). |
| `createFixtureTerminalHost(options)` | `zidane-ui/terminal/testing` | Memory: echoing shells, for demos and tests |
| Your own | `zidane-ui/terminal/headless` | Anything that has a PTY |

| Member | Gives |
| - | - |
| `state(scope)`, `open(scope, options)`, `close(id)` | A scope's terminals, opening one, closing one (an agent's: ends its task) |
| `attach`, `detach`, `write`, `resize`, `ack` | One view's link to one terminal: history, typing, its grid size, flow control |
| `listen(sink)` | Lists that changed, output, resets, and relinks (every scope reloads, every view attaches again) |
| `restart(id)`, `setHidden(id, hidden)` | Restart a process; hide a session's terminal from its agent |
| `adoptDraft(draftId, sessionId)`, `releaseDraft(draftId)` | A draft's terminals move into its session, or back to the project |
| `layouts` | Where the drawer, the tab order and the agent filter are kept per scope. Default: the page. |

`describeTerminalHostContract` (`zidane-ui/terminal/testing/contract`) is the suite every host runs: yours can run it too.

## How a view draws

- Output never goes through React state. The store hands it straight to each view. A view keeps its own cursor and acknowledges what it wrote.
- The grid always has the PTY's size. A view only asks for what fits its container. The view being looked at gets that size.
- A view stays transparent until it has its history and its size, so a half-drawn terminal never shows. Big replays are parsed in 8 ms slices.
- Two views of one terminal are fine (the drawer and the panel tab). Each one attaches with its own client id.
