# Terminal Plugin Spec

This document describes the Terminal plugin implementation, architecture, and integration points. It is a reference for continuing work later.

## Goals

- Provide a browser terminal (xterm.js) inside Officer.
- Support two execution modes:
  - Host mode: shell runs on the host machine.
  - Sandbox mode: shell runs inside a Docker sidecar container.
- Keep host mode working and optional; sandbox is enabled via admin settings.
- Keep all terminal logic self-contained in the plugin.
- Allow the terminal UI to be rendered anywhere with a reusable component.

## File Layout

- `src/workspaces/plugins/Terminal/index.ts`
  - Exports plugin metadata, client screen, and server websocket handler.
- `src/workspaces/plugins/Terminal/client/TerminalView.tsx`
  - Reusable terminal component (xterm + websocket bridge).
- `src/workspaces/plugins/Terminal/client/Screen/index.tsx`
  - Page wrapper that renders `TerminalView` inside `DashboardLayout`.
- `src/workspaces/plugins/Terminal/client/index.ts`
  - Exports `Screen` and `TerminalView`.
- `src/workspaces/plugins/Terminal/server/websocket.ts`
  - WebSocket handler for terminal sessions and sidecar orchestration.
- `src/workspaces/plugins/Terminal/server/pty-sidecar.mjs`
  - Node sidecar process that owns `node-pty` and shell spawn.
- `src/workspaces/plugins/Terminal/server/Dockerfile.terminal-sidecar`
  - Docker image for sandbox mode.
- `src/workspaces/plugins/Terminal/server/templates/.zshrc`
- `src/workspaces/plugins/Terminal/server/templates/starship-officer.toml`
  - Template configs copied into user home for stable shell UX.

## Client Architecture

### TerminalView

`TerminalView` is the reusable component that can be rendered anywhere.

Props:

- `className?: string`
- `style?: CSSProperties`
- `wsPath?: string` (default: `/api/terminal/ws`)
- `fontSize?: number` (default: 14)
- `fontFamily?: string`
- `theme?: { background?; foreground?; cursor?; selectionBackground? }`
- `autoFocus?: boolean` (default: true)
- `onReady?: (term: XTerm) => void`
- `onExit?: () => void`
- `onDisconnect?: () => void`

Behavior:

- Initializes xterm after mount for StrictMode compatibility.
- Connects to websocket at `wsPath`, appending `token` from localStorage.
- Sends `resize` on open, and on ResizeObserver changes.
- Forwards terminal input as `{ type: 'input', data }` JSON.
- Writes server output on `{ type: 'output', data }`.
- Emits `onExit` when `{ type: 'exit' }` is received.
- Emits `onDisconnect` when the websocket closes.

### Screen

`Screen` is a thin wrapper around `TerminalView` for the full-page route:

```
<DashboardLayout>
  <TerminalView className="h-full w-full p-2" />
</DashboardLayout>
```

This matches plugin conventions used by FileBrowser.

## Server Architecture

### WebSocket Flow

The terminal websocket is mounted in `src/server.tsx`:

- Path: `/api/terminal/ws`
- Auth: `token` query parameter, validated with `verify()`.
- Server handler: `terminalWebsocket` from `plugins/Terminal/server`.

Message formats between client and server:

- Client -> server:
  - `{ type: 'input', data: string }`
  - `{ type: 'resize', cols: number, rows: number }`
- Server -> client:
  - `{ type: 'output', data: string }`
  - `{ type: 'exit' }`

### Sidecar Model

The Bun server does not spawn PTYs directly. Instead it proxies to a Node sidecar
that runs `node-pty` for reliability.

There are two sidecar modes:

1. Host mode
   - Spawns a local Node sidecar on the host via Bun.
   - Sidecar binds to `127.0.0.1`.

2. Docker sandbox mode
   - Runs a Docker container per user.
   - Container binds to `0.0.0.0` and is mapped to a host port.
   - Container runs the same Node sidecar and shell inside the container.

### Mode Selection

- Settings are stored in `~/.config/officer.dev/server-settings.json`.
- `terminalSandboxed: true` enables Docker mode.
- If `terminalSandboxed` is falsy, host mode is used.

### Sidecar Handshake

On websocket open:

- Determine user home: `getHomeDir(email)`.
- Ensure the home directory exists.
- Determine mode (host or docker).
- Connect to sidecar on a port (host: default 5337; docker: user-specific).
- Send init payload to sidecar:

```
{
  type: 'init',
  shell: { command, args, name },
  cwd,
  homeDir,
  userLabel
}
```

- `userLabel` is the email used by the prompt template.
- `cwd` and `homeDir` are `/home/officer` in Docker, or the user home on host.

### Docker Container Lifecycle

Containers are named `officer-terminal-${userId}` and stored per user in:

`data/terminal-containers.json`

Rules:

- Reuse an existing container if it exists and is running.
- Restart the container if it exists but stopped.
- Start a new container if none exists; allocate a port starting at 54000.
- Containers are left running for reuse when websocket closes.

### Docker Image

- Tag: `officer-terminal-sidecar:v1`
- Built from `Dockerfile.terminal-sidecar` when missing.

Includes:

- zsh, git, curl, ca-certificates
- oh-my-zsh
- starship
- fortune-mod, cowsay
- eza
- node, ws, node-pty

### Shell Configuration

Templates are copied into the user's home if missing:

- `.zshrc`
- `.config/starship-officer.toml`

This provides a stable base for all users while allowing per-user customization.

Prompt requirement:

- Format: `email@domain:officer.dev`
- Email colored in button blue (#0891B2)

## Plugin Conventions

- `index.ts` exports `plugin` metadata with `id` matching folder name.
- Client exports use `Widget`/`Screen` conventions. Terminal only exports `Screen`
  and `TerminalView` (no widget).
- Server exports a websocket handler (not a router). This is a special-case
  plugin that is mounted in `src/server.tsx`.

## Known Constraints

- Terminal mode is global via server settings; no per-component mode selection.
- `TerminalView` builds the websocket URL from `window.location` and localStorage
  token; custom auth is not supported yet.
- Sidecar connection retries are bounded; errors are printed to the terminal.

## Potential Future Enhancements

- Allow `TerminalView` to request mode/cwd via websocket init payload.
- Add a read-only mode or initial command support.
- Add per-component theme presets or allow app-level defaults.
- Add better telemetry/logging for sidecar startup failures.

## API Contract

This section describes the messages across each layer.

### Client <-> Server (Bun)

Client -> server:

- `input`
  - `{ type: 'input', data: string }`
  - `data` is raw terminal input from xterm.
- `resize`
  - `{ type: 'resize', cols: number, rows: number }`

Server -> client:

- `output`
  - `{ type: 'output', data: string }`
  - `data` is raw PTY output to be written to xterm.
- `exit`
  - `{ type: 'exit' }`
  - Signals terminal process termination.

### Server <-> Sidecar (Node)

Server -> sidecar:

- `init`
  - `{ type: 'init', shell, cwd, homeDir, userLabel }`
  - `shell`: `{ command: string; args: string[]; name: string }`
  - `cwd`: initial current working directory
  - `homeDir`: home directory to set in the sidecar
  - `userLabel`: email for the prompt template
- `input`
  - `{ type: 'input', data: string }`
- `resize`
  - `{ type: 'resize', cols: number, rows: number }`

Sidecar -> server:

- `output`
  - `{ type: 'output', data: string }`
- `exit`
  - `{ type: 'exit' }`

## Sequence Diagram

```
Client (xterm)     Bun Server          Sidecar (Node)          Shell
     |                 |                     |                  |
     | WS connect      |                     |                  |
     |---------------> |                     |                  |
     |                 | connect sidecar     |                  |
     |                 |-------------------> |                  |
     |                 | init (shell/cwd)    |                  |
     |                 |-------------------> | spawn PTY        |
     |                 |                     |----------------->|
     | input           |                     |                  |
     |---------------> | input               |                  |
     |                 |-------------------> | write to PTY     |
     | output          |                     |                  |
     |<--------------- | output              | read from PTY    |
     |                 |<------------------- |                  |
     | resize          |                     |                  |
     |---------------> | resize              |                  |
     |                 |-------------------> | set PTY size     |
```

## Docker Troubleshooting

Common failure modes and checks:

- Docker not installed or not in PATH
  - `ensureDockerImage()` will throw. Install Docker or fix PATH.

- Image build fails
  - Build uses `Dockerfile.terminal-sidecar` and tag `officer-terminal-sidecar:v1`.
  - Check for network access (apt), and that build context is the plugin folder.

- Container starts but sidecar is unreachable
  - Container binds to `0.0.0.0`, host maps `127.0.0.1:${port}:${port}`.
  - Port is user-specific, stored in `data/terminal-containers.json`.
  - Verify with `docker ps` and `docker logs` for the container name.

- Permissions inside container
  - Container runs with host UID/GID derived from the mounted user home.
  - If home dir ownership is incorrect, zsh history may fail to write.

- Shell missing
  - Container must have zsh installed; the Dockerfile includes zsh.
  - Host mode resolves shell from `$SHELL` or known paths.

## Per-User Template Rules

Templates are copied into user home on first launch only.

Files:

- `.zshrc`
- `.config/starship-officer.toml`

Rules:

- If a file already exists, it is not overwritten.
- This lets the admin provide a stable default while allowing user customization.
- To reset a user to defaults, delete the per-user files and reconnect.
