# How to work with PocketTUI

## 1. Finding the backend from inside a tmux session

| What | Where | Notes |
|---|---|---|
| Install dir | `~/pockettui` (or `$POCKETTUI_DIR` at install time) | `install.sh:37` |
| Port | default `5560`; the running value is in `~/pockettui/runtime.json` | `runtime.json` = `{version, pid, host, port, started_at}`, written at start (mode 0644) and removed on a clean exit; a `kill -9` leaves it behind, so check that `pid` is alive (`app.py:11169-11196`). The wrapper `~/.local/bin/pockettui` also carries `PORT="..."` (`install.sh:903`). |
| Bind address | `0.0.0.0` by default (`app.py:11241`); the service unit passes only `--port` (`install.sh:1381`) | From the same machine, use `http://127.0.0.1:<port>`. The `tailscale serve` path (`https://<host>.<tailnet>/pockettui`) is for remote devices only. |
| Token | `~/pockettui/.token`, 10 base32 chars + newline, mode 0600 (`app.py:76`, `app.py:257-267`, `install.sh:1265-1298`) | Readable by the agent: tmux is spawned by `app.py` (or already runs as the same user), so the pane runs under the same uid that owns the file. No env var carries it into sessions (`new-session` sets only `ZDOTDIR`, `app.py:3161`). |
| Header | `X-PocketTUI-Token: <token>` on every `/api/*` request (`app.py:154`, `507-539`) | No loopback exemption. A server started with `--no-auth` (loopback bind only) accepts requests without it. |

Rules for the token:
- Read it only when a route in section 4 is needed, and never print it to the pane, a log, or a commit. Every device attached to the session sees the pane, and the code is the only thing guarding a full shell.
- A wrong token answers `401 {error:"bad token", hint}` and counts toward the per-IP backoff (5 free tries, then doubling delays up to 300 s, `app.py:296-368`). Do not retry in a loop.
- Mutating session routes share a limit of 20 calls per 60 s per IP (`RATE_SESSION_MUTATE`, `app.py:410`).
- Routes that take `dev` resolve to that device's own grouped view. An agent has no device name, so leave `dev` out and the base session is used.

Minimal probe:
```sh
PORT=$(python3 -c 'import json;print(json.load(open("'"$HOME"'/pockettui/runtime.json"))["port"])' 2>/dev/null || echo 5560)
curl -s http://127.0.0.1:$PORT/.well-known/pockettui
curl -s -H "X-PocketTUI-Token: $(cat ~/pockettui/.token)" http://127.0.0.1:$PORT/api/version
```

## 2. Discovery

`GET /.well-known/pockettui`: no token. Returns `{"product":"pockettui","version":"<VERSION or "">"}` and nothing else (no hostname, port, paths or capabilities). The phone and the installer use it as a reachability probe (`app.py:1268-1283`).

`GET /api/version`: token. Returns `{"version": str, "host": socket.gethostname(), "capabilities": {flag: bool}}` (`app.py:1463-1490`). The map is built once at import (`server_capabilities()`, `app.py:1378-1462`), so a Chromium or ffmpeg installed later shows up only after a service restart. A flag that is absent or false means the feature is not there.

| flag | meaning | value |
|---|---|---|
| `fs` | `/api/fs/*` explorer and editor | always true |
| `image_paste` | `/api/image` | true |
| `upload` | `/api/upload` (composer "+") | true |
| `voice` | `/api/transcribe`, `/api/voice_status` (the live engine is voice_status's answer) | true |
| `learned` | `/api/learned` | true |
| `push` | Web Push available | false without `pywebpush` |
| `dbg` | `/api/dbg` | true |
| `git` | `/api/git/changes|diff|apply` | true |
| `git_ref` | `/api/git/branches` and `ref=` on `/api/fs/list|read` | true |
| `search` | `/api/search` | true |
| `ping` | attach socket answers `{"type":"ping"}` | true |
| `update` | `/api/update` | false with no `pockettui` wrapper or no tmux |
| `pair_qr` | `/api/pair_qr.svg` | true |
| `update_status` | `/api/update_status` | true |
| `type` | `/api/session/type` | true |
| `relay` | `/api/relay` | true |
| `browse` | `/b/<tok>/...` proxy | false without `httpx` |
| `browse_tab` | `POST /api/browse {mode:"tab"}` | true |
| `bookmarks` | `/api/browse/bookmarks` | true |
| `zip_dir` | a folder at `/api/fs/download` streams a zip | true |
| `thumbs` | `/api/fs/thumb` | false without ffmpeg |
| `browser_full` | `/ws/browser/<pane>`, `/api/browser/status` | false without a Chromium |
| `pdf_thumbs` | PDF page-one thumbnails | false without pdftoppm, gs or sips |

## 3. Terminal-side conventions

Nothing in this section is an API call: it is how the agent's printed output behaves on the user's device. Printing something never opens it; the user taps it.

### 3.1 Local paths (`08-links.js:13`, `trimPath` `:225`)
```
LOCAL_PATH_RE = /(?:^|[\s"'`(\[{<=:,])((?:~\/|\/)[^\s"'`()\[\]{}<>:;,]+)/g
```
- The path must start with `/` or `~/`, at the start of a line or right after whitespace, a quote, a backtick, `(`, `[`, `{`, `<`, `=`, `:` or `,`. A bare `logo.png` or a relative `src/app.py` is not a link.
- The path ends at whitespace, a quote, a backtick, any bracket, `<`, `>`, `:`, `;` or `,`. So `app.py:42` links only `app.py`, and a path containing spaces is cut at the first space. Trailing `. ! ? ' " $ % #` are trimmed, and a bare `/` is dropped.
- Wrapped paths are rejoined across up to 8 rows, including continuation rows indented by up to 4 spaces (`logicalLine`, `stitchedText`).
- A `https?://` URL on the same row takes precedence over any path inside it.

What a tap opens (`activateLink` `08-links.js:240`, `openEntry` `28-file-explorer.js:1840-1866`):

| printed path | opens |
|---|---|
| `.png .jpg .jpeg .gif .webp .svg .bmp .mp4 .webm .mov` (`FILES_MEDIA_RE`) | full-screen image/video viewer |
| `.pdf` | PDF view in the docked pane (laptop) or a new browser tab (phone) |
| `.md .markdown` | rendered Markdown reader (tables, task lists, `$…$`/`$$…$$` KaTeX); Edit swaps to the editor |
| `.html .htm .xhtml` | rendered page in a new tab from a signed, sandboxed address; relative links to sibling files resolve |
| a directory | file explorer at that folder |
| any other file | CodeMirror editor (read-write; `.vimrc` honoured in Vim mode) |

A path that does not exist gets a "Couldn't find" toast. Media and PDF paths are not previewed at a git ref.

### 3.2 URLs, `http://localhost:PORT` and the relay (`08-links.js:18`, `:320-408`)
- ``URL_RE = /https?:\/\/[^\s"'`<>]+/gi``; trailing `. , ; : ! ? ' "` and unbalanced closing brackets are trimmed.
- **Laptop (wide layout) with the `browse` capability:** every http(s) URL opens in the in-app browser pane, fetched by the computer, so `http://localhost:PORT` works there directly with no relay.
- **Phone (or a laptop without `browse`):** the URL opens in a new browser tab. If the host is `localhost`, `127.0.0.1`, `[::1]`, `::1` or `0.0.0.0`, the phone first rewrites it to the computer's address as the phone knows it, then fires `POST /api/relay {host, port}` without waiting. The backend binds that port number on that address and forwards it to the loopback listener. It answers `relayed`, `direct` (something already listens there) or refuses (`not_listening` if nothing is on loopback at that port, `bad_host` for a loopback or wildcard target).
- What the agent should do: print `http://localhost:<port>` after the server is actually listening. It needs no API call. The relay binds on tap, not on print, and a server bound to `0.0.0.0` is reachable as printed.

### 3.3 Pane title and cwd
- **Session row label:** set the pane title with OSC 2 (or OSC 0): `printf '\033]2;%s\033\\' 'Running tests'`. The list strips leading non-alphanumeric glyphs and shows the rest. A title of the form `user@host: /path`, or equal to the hostname, is ignored and the folder name is shown instead (`06-session-list.js:250-263`, `app.py:711-716`).
- **Explorer "open at this pane's folder" and docked-pane follow:** for a local pane this is tmux `pane_current_path`, the cwd of the pane's foreground process (`pane_cwd`, `app.py:786-829`). A `cd` typed at the shell moves it; a `cd` inside an agent's own tool subprocess does not. To point the user at a folder, print its absolute path.
- **ssh panes only:** when the pane's foreground command is `ssh`, the title is read as `user@host: /abs/path` (everything after the first colon; `~` paths refused). A global tmux hook (`pane-title-changed`, `TITLE_HOOK`, `app.py:741-775`) copies every title that matches `*@*: /*` in an ssh pane into the pane option `@ptui_remote_cwd`, so a TUI that later overwrites the title does not erase it. The path is used only if it exists on this machine, directly or through `POCKETTUI_PATH_REWRITES`. For this to work, the remote shell must have drawn such a title in that folder before the TUI started.

### 3.4 How "waiting", "ready" and "idle" are derived (`app.py:10170-10780`)
The watcher polls every 2 s. A session is `active` while its newest output is less than 6 s old (`POCKETTUI_NOTIFY_IDLE_S`). At the busy-to-idle edge it reads the visible pane once, if the busy episode lasted 10 s or more or a non-shell program still holds the pane. `detect_prompt` then checks the last 5 non-empty lines above an empty composer, in this order:

1. `[y/n]`, `(y/n)` or `yes/no` → **waiting**, chips `y` `n`.
2. Two or more adjacent lines matching `^\s*│?\s*(❯\s*)?\d+[.)]\s` (numbered menu, box border tolerated) → **waiting**, chips are the first four digits.
3. Exactly one line `❯ <text>` with a non-empty line under it (unnumbered chooser) → **waiting**, no answer chips.
4. `do you want | would you like | proceed? | continue? | are you sure` → **waiting**, chips `y` `n`.
5. The cursor line is a composer (`>` or `❯`, optionally after `│`) that is empty, or holds only dim (SGR 2) placeholder text → **ready**. Non-dim text in it → drafting, shown as **idle** and never notified.
6. The cursor line ends in `?` → **waiting**, no answer chips.
7. Otherwise → **idle**.

Chips always add Enter (`\r`) and Esc. A chip tap sends only that key, with no Enter after `y` or a digit, and the chips come down as soon as the pane prints again.

Notifications (Web Push and/or ntfy) go out only for sessions whose `@notify` is `on` or `quiet`. At most one goes out per 30 s per session, and a repeat of the same text is suppressed:
- waiting: at once;
- ready: only after 30 s or more of work (`POCKETTUI_NOTIFY_READY_BUSY_S`);
- "<cmd> finished": a run of 10 s or more that returned to a shell;
- "went quiet — may need input": a program went silent after 10 s or more of work;
- **terminal bell**: `printf '\a'` notifies immediately, with no text guard, provided tmux sets the window's bell flag (tmux may not flag a bell in a window a client is currently viewing).

For an agent to be detected correctly:
- Ask with a literal `(y/n)` or a numbered list (`1. …`, `2. …`), then **stop printing**. Spinners, clocks or status lines that repaint keep the session `active` and defer detection.
- Keep the question within the last 5 non-empty lines above the input line.
- When done, return to an empty prompt/composer; a plain shell prompt after a long command reads as finished.
- Keystrokes the user sends from a phone reset the repeat guard; typing at the physical keyboard does not.
- There is **no route that marks a session waiting or sends a push**. `POST /api/notify` only stores the per-session preference.

### 3.5 Files that arrive from the user
- Pasted image: `~/.pockettui/images/paste-<stamp>-<hex>.<ext>`. The type is sniffed from the bytes and the newest 30 are kept (`app.py:8660-8735`). The absolute path is typed at the user's cursor.
- Attached file: `~/.pockettui/uploads/<name>-<stamp>-<hex>.<ext>`, newest 30 kept (`app.py:8766-8837`).
- Browser-pane upload for a streamed page: `~/.pockettui/uploads/browser/<hex>/` (`app.py:8126`). Streamed downloads: `~/.pockettui/downloads`.

## 4. Documented HTTP routes an agent may use

All need `X-PocketTUI-Token` except `/.well-known/pockettui`. None of them changes what is on the user's screen.

```
GET  /.well-known/pockettui   (no token)                    -> {product:"pockettui", version}
GET  /api/version             -                             -> {version, host, capabilities:{<flag>: bool}}
GET  /api/sessions            -                             -> {sessions:[{name, created, attached, windows, command, title, cwd, alias, notify, state: active|waiting|ready|idle, last_activity}]}
POST /api/session             {name, dir?}                  -> {session} | 400 {error} (name required, <=60 chars, no "." or ":", unique; dir defaults to ~)
POST /api/alias               {session, alias}              -> {session, alias}   (display name only; "" clears)
POST /api/session/rename      {session, name}               -> {session}           (real tmux rename; drops every device view)
POST /api/session/type        {name, dev?, text}            -> {session, chars} | 400 empty | 404 | 413 too_long (>4096)  (typed with send-keys -l, \r and \n stripped, never submitted)
POST /api/notify              {session, mode: off|on|quiet} -> {session, notify}  (preference only; sends nothing)
POST /api/session/kill        {session}                     -> {killed}           (whole group; only on explicit user request)
GET  /api/update_status       -                             -> {state: <install.sh update-state.json> | null, session_alive: bool}
```
Prefer the CLI where one exists: `pockettui update | version | status | browser install|check|status`. Prefer plain tmux for reading your own pane.

## Stay attached to your work

### See what each session is doing
Set your pane title with an OSC 2 (or OSC 0) escape, e.g. printf '\033]2;%s\033\\' 'Refactoring auth'; the row shows it on the next list refresh with leading symbols stripped. A title shaped like user@host: /path, or equal to the hostname, is ignored and the row shows the folder name instead.

### Use one session from several devices
Sessions named <device>-<session> (or <device>-<session>-2..9, or ptui-<session>) grouped onto a user's session are PocketTUI's per-device views; tmux ls lists them as grouped. Do not attach to, rename or kill them: they are recreated on every connect and swept when stale.

### Rename a session
Sets the display name every device shows without renaming the session (an empty alias clears it), same as tmux set-option -t <session> @alias '<text>'. POST /api/session/rename {session, name} instead changes the real tmux name and drops every device's view of it, so use that only when asked.
`POST /api/alias  {session, alias}  -> {session, alias}`

### Close a session
Only on the user's explicit request: kills the session and every device view grouped onto it, with no undo. A plain tmux kill-session on the base session would leave the device views alive.
`POST /api/session/kill  {session}  -> {killed}`

### See every running session
Read the list with the pairing token to see every session's title, cwd and watcher state (active, waiting, ready, idle). It is read-only: nothing changes on the user's device.
`GET /api/sessions  -  -> {sessions:[{name,created,attached,windows,command,title,cwd,alias,notify,state,last_activity}]}`

### Start a new session
Creates a detached tmux session (name required, max 60 chars, no '.' or ':'; dir defaults to ~). It appears in the user's list on the next refresh but does not open on their screen; the sess-MMDD-HHMM default name is the app's, not the route's.
`POST /api/session  {name, dir?}  -> {session}`

## Type and talk from a phone

### Install speech engines on the computer
Run bash ~/pockettui/setup_voice.sh --parakeet, --whisper or --all (no prompts; piped with no flag it installs both). Nothing leaves the machine; the phone's Settings > Dictation rows turn available once /api/voice_status reports the engine.

### Attach any file from the phone
An attached file is saved as ~/.pockettui/uploads/<name>-<stamp>-<hex>.<ext> (newest 30 kept) and its absolute path is typed into the user's message; images go through the image-paste path instead.

### Paste a screenshot for an agent
A pasted image is saved as ~/.pockettui/images/paste-<stamp>-<hex>.<ext> (type sniffed from the bytes, newest 30 kept) and that absolute path is typed at the user's cursor; read the file at that path to see the image.

### Write a message before sending
Text the user sends from the box arrives in your pane as one bracketed paste followed by Enter, so a multi-line message is a single submission, never line by line.

## Files and code

### Follow a folder over ssh
Only for a pane whose foreground command is ssh: the remote shell must set its title to user@host: /absolute/path before any TUI takes over (tmux keeps the last such title in the pane option @ptui_remote_cwd). The path is used only if it exists locally, directly or through POCKETTUI_PATH_REWRITES; ~ paths are refused.

### File browser follows the terminal
The pane follows tmux's pane_current_path, which is the cwd of the pane's foreground process: a cd typed at the shell moves it, but a cd inside an agent's own subprocess does not. Print the folder's absolute path instead so the user can tap it.

### No overwriting others' changes
If you write a file the user has open in the editor, their next Save gets a conflict prompt (Overwrite or Reload) instead of silently replacing your change; nothing is shown to them until they press Save.

### Review your code changes
The pane lists the repository that contains the session pane's cwd, so edits you make there show up for the user to stage or revert per block; it refreshes by polling, you do not need to signal it.

### Tap printed file paths
Print absolute or ~/ paths (e.g. /home/u/out/report.md) and the user can tap them; printing does not open anything. The path must start at line start or after whitespace, a quote, ( [ { < = : or a comma, and stops at whitespace, quotes, brackets, : ; , so file.py:42 links only file.py; wrapped lines are stitched back together.

### Read Markdown nicely formatted
Write a .md file (tables, task lists, $...$ and $$...$$ maths, images by relative path) and print its absolute path; the user taps it and reads it rendered. The file's content can never run script in the reader.

### View printed images and videos
Print the absolute path of an image or video you produced (png, jpg, jpeg, gif, webp, svg, bmp, mp4, webm, mov); the user taps it to view it full-screen. A printed .pdf path opens the PDF view, .md the rendered reader, .html a sandboxed page in a new tab, and anything else the editor.

## Preview and browse

### Open dev servers on your phone
Print http://localhost:PORT (or 127.0.0.1 / 0.0.0.0) for a server listening on loopback. On tap the phone rewrites the host to the computer's address and asks the backend (POST /api/relay {host, port}) to relay that port number to your loopback listener; printing alone opens and binds nothing, and nothing is relayed until the server is actually listening.

### Terminal links open beside it
On a wide layout every http(s) URL you print opens in the in-app browser pane, fetched by the computer, so http://localhost:PORT works there directly with no relay.

### Real Chrome for difficult sites
If the user wants streamed pages, run pockettui browser install (then pockettui browser check / status) on the computer; the pane offers the mode once the server reports browser_full, which is frozen at server start, so the service must restart.

### Open an HTML report
Write a self-contained .html report and print its absolute path; tapping opens it rendered in a new tab from a signed, sandboxed address, and relative links to sibling files resolve.

### Tap printed web links
Print full http:// or https:// URLs; trailing punctuation and unbalanced closing brackets are trimmed. Printing does not open anything; the user taps it.

## Connect computers and devices

### Get fix commands typed for you
Types literal text at the prompt of a named session's active pane; every \r and \n is stripped so it never runs, and the user must press Enter. Use it to stage a command for the user in another session (max 4096 chars); it does not create a session and does not switch the user's screen.
`POST /api/session/type  {name, dev?, text}  -> {session, chars}`

### Keeps running in the background
Linux: systemctl --user status pockettui and journalctl --user -u pockettui; macOS: the launchd agent logs to ~/pockettui/pockettui.log. Restarting the service keeps tmux sessions alive (KillMode=process).

### Manage PocketTUI from the command line
Commands: pockettui update [--allow-downgrade], pockettui version, pockettui status, pockettui browser install|check|status. Installed at ~/.local/bin/pockettui; none of them touch the user's device directly.

### Update the computer from your phone
Run pockettui update on the computer (--allow-downgrade to go back). The service restarts but tmux sessions survive; from the app the same update runs in a tmux session named pockettui-update, and GET /api/update_status reports {state, session_alive}.

### See which version is running
Run pockettui version (or pockettui status) on the computer; GET /api/version returns the same server version with the capability map.

### Pair with a one-time code
The code lives in ~/pockettui/.token (mode 0600, readable by the user the service runs as). Send it as the X-PocketTUI-Token header on /api/* calls; never print it into the terminal, since every device attached to the session would see it.

## Know when it needs you

### Send alerts to ntfy
Set POCKETTUI_NTFY_URL (and optionally POCKETTUI_APP_URL for the tap-through link) in the service's environment and restart it; the same notifications as push go to that topic, lower priority in quiet mode.

### Alerts on a locked phone
A push fires only for sessions whose @notify is on or quiet, at most once per 30 s per session: when a question is detected, when an agent returns to an empty composer after at least 30 s of work, when a command of 10 s or more finishes back at the shell, or at once when the pane rings the terminal bell (printf '\a', subject to tmux flagging the bell). No route sends one.

### See which session needs you
State is read from your pane after output stops for 6 s: a y/n or yes/no question, 'do you want / would you like / proceed? / continue? / are you sure', two or more numbered options (1. 2.), or one ❯-marked choice over another line reads as waiting; an empty ❯ or > composer reads as ready; anything else is idle. There is no route to set it; end a question with a clear '(y/n)' or a numbered list and then stop printing.

### Answer a prompt with one tap
A detected y/n question gets y and n chips, a numbered menu gets its first four digits, and an unnumbered chooser or trailing question gets only Enter and Esc (always added). A tap sends just that key to your pane, with no Enter after a digit or letter; the chips go away as soon as the pane prints again.

### Get notified by a session
This only stores the session's notification preference (tmux option @notify: off, on or quiet), like tmux set-option -t <session> @notify on; it sends nothing. Change it only when the user asks.
`POST /api/notify  {session, mode: off|on|quiet}  -> {session, notify}`

## Make it yours

### Edit with Vim keys
The editor reads ~/.vimrc when Vim mode is switched on and applies the set options and map/noremap lines it can translate; edits you make to ~/.vimrc apply the next time the user turns Vim mode on (plugins and functions are skipped).

## Under the hood

### Check a PocketTUI server answers
No token needed; answers only product and version, no hostname, paths or capabilities. Use it to confirm the local backend (default http://127.0.0.1:5560) is up before an authenticated call.
`GET /.well-known/pockettui  (no token)  -> {product:"pockettui", version}`

### Device logs reach the computer
Read them as lines 'dbg[<device>] <text>' in journalctl --user -u pockettui (Linux) or ~/pockettui/pockettui.log (macOS); nothing is stored elsewhere and nothing arrives unless the user has switched Debug log on.

### Keep recordings for testing
With POCKETTUI_VOICE_KEEP=<n> set, the newest n takes are kept in ~/pockettui/.voice_takes/<stem>.{orig,wav,json}; nothing in the app reads them back.

### App adapts to older servers
Read the capability map to learn which features this server has (a missing or false flag means absent): fs, image_paste, upload, voice, learned, push, dbg, git, git_ref, search, ping, update, pair_qr, update_status, type, relay, browse, browse_tab, bookmarks, zip_dir, thumbs, browser_full, pdf_thumbs. It is frozen at server start.
`GET /api/version  -  -> {version, host, capabilities:{<flag>: bool}}`
