- Approval: re-prompt unknown answers (max 3), shell-only affirmative guidance recovery; destructive actions stay strict - GoalVerifier subagent with strict JSON verdicts, bounded follow-up rounds, nonzero exit plus open turn on unverified one-shots - Retrieval eval (Hit@K/MRR/nDCG/Context Precision), FTS cascade with title weighting, query/read event logging, --eval-retrieval CLI - Supervisor caps (8 parallel, 64 records) and scheduler hardening (4 fires/tick, per-schedule lease, stated misfire policy) - Sandbox box resource trio: 2g/2.0/256 at create, best-effort update for existing boxes, rm -f recovery pointer - Research archive: 12 subjects, 667 sources, 27 conclusions; suites 979 + 16 green
239 lines
16 KiB
Markdown
239 lines
16 KiB
Markdown
# grr in the terminal: `tai.py --tui`
|
|
|
|
`./tai.py --tui` is the grr app (the phone-first web client in the sibling `grr` project) as a fullscreen terminal
|
|
program: same bots, chats, automations, library, projects, settings, approvals and computers, same words, same
|
|
colors, from the same server. It is a client. Nothing is stored in tai's vault; the bots, chats and files live in the
|
|
grr server, so the web app and the terminal show the same state at the same moment.
|
|
|
|
Standard library only, in `tai.py` like everything else, built on the retained TUI framework in [tui.md](tui.md).
|
|
The Telegram-style chat scene on the local vault is still there as `./tai.py --tui-local`.
|
|
|
|
`GrrSpecTests` in `test_tai.py` fails when the tables below drift from the code.
|
|
|
|
## Run it
|
|
|
|
```
|
|
grr-serve # the server, http://127.0.0.1:8765 (or: python3 -m grr serve)
|
|
./tai.py --tui # the client
|
|
GRR_URL=http://host:8765 ./tai.py --tui
|
|
```
|
|
|
|
First start shows the sign-in form (DevPlace username or email and password, or a pasted DevPlace key, or the key of
|
|
the server's own session when the server says it allows that). The grr session token, not the DevPlace key, is kept in
|
|
`<TAI_HOME>/grr.json` (mode 0600) together with the server address and every signed-in account. `GRR_URL` beats the
|
|
saved address. `--theme` and `TAI_THEME` belong to the other scenes: here the theme is the account's (Appearance). When the server cannot be reached the screen says so, shows the address and offers Try again and Change
|
|
address. `Ctrl+C` always quits.
|
|
|
|
## What it takes from the server
|
|
|
|
One source of truth, nothing copied into tai:
|
|
|
|
- `GET /api/spec`: caps, modes, tiers, shapes, themes, variants, languages, schedule kinds, rule actions, pane tabs,
|
|
palette. The TUI reads the palette from here (night, day, Black and Blue variants), so a brand change on the server
|
|
is a brand change here.
|
|
- `GET /locales.json`: the string table, in the account's language (`system` follows `LC_ALL`, `LC_MESSAGES`, `LANG`).
|
|
Every word on screen is one of these keys. Four sort labels the server does not serve (`library.sort_*`) have a
|
|
built-in English fallback (`GRR_STRING_FALLBACK`); any other missing key shows as the key.
|
|
- `GET /api/events` (server-sent events) on a thread of its own: `message.new`, `message.update`, `status` and the
|
|
other events listed under Events. It reconnects every `GRR_SSE_RETRY` seconds and re-reads bots and chats when the
|
|
link comes back, like the web app does.
|
|
- Every other screen is one or two REST calls. The same error codes the web app words (`apierr.*`) are worded here.
|
|
|
|
## Layout and responsiveness
|
|
|
|
Terminals are small phones too. One pane below `GRR_SIDE_MIN_COLS` columns (the drawer is the home screen, every other
|
|
screen replaces it, Esc goes back); two panes from there on (the drawer `GRR_SIDE_W` columns wide, the screen beside
|
|
it, a welcome mark when nothing is open). Tabs that do not fit fold into one dropdown chip, the mode bar of a general
|
|
chat folds into a dropdown, tiles reflow into fewer columns, texts wrap. Nothing is wider than the terminal at 30
|
|
columns (a test draws every screen at 30, 56, 100 and 160 columns and fails on any overflow).
|
|
|
|
The side pane of a bot chat (Preview, Files, Deploy) sits beside the chat from `GRR_PANE_MIN_COLS` columns and over it
|
|
below that. Cards are rounded with half-block edges and quarter-block corners, so they look rounded in any font. Colors
|
|
are the grr palette as truecolor values; `COLORFGBG` decides what `system` means (night unless it says light).
|
|
|
|
## Screens
|
|
|
|
Every screen is one of these routes, the same patterns as the web app's.
|
|
|
|
| Route | View | The screen |
|
|
|---|---|---|
|
|
| `/bots` | `GrrBotsView` | Bots and group chats, the avatar strip, new bot and new group chat |
|
|
| `/bot/:id` | `GrrProfileView` | Profile: name and description, Info (character shape and color, instructions, memories, skills, routines, voice, notifications, auto-approve, share as template), Documents, Computer, Links |
|
|
| `/bot/:id/instructions` | `GrrInstructionsView` | Instruction, persona and rules, each with reset to default |
|
|
| `/bot/:id/memories` | `GrrMemoriesView` | Memories list and add |
|
|
| `/memory/:id` | `GrrMemoryView` | One memory, edit and delete |
|
|
| `/bot/:id/skills` | `GrrSkillsView` | Skills list and add |
|
|
| `/skill/:id` | `GrrSkillView` | One skill, edit and delete |
|
|
| `/bot/:id/computer` | `GrrComputerView` | Take control: the bot computer's live shell |
|
|
| `/bot/:id/screen` | `GrrScreenView` | The graphical screen: start, stop and the address to open it |
|
|
| `/chat/:id` | `GrrChatView` | Bot, group, general and private chats; `new` makes a general chat on the first send |
|
|
| `/automations` | `GrrAutomationsView` | Routines as cards, run now, pause, delete |
|
|
| `/automations/new` | `GrrNewAutomationView` | Name, schedule or trigger, instruction, bot, tier, notifications |
|
|
| `/automation/:id` | `GrrAutomationView` | One routine: active, instruction, schedule, bot, notifications, last run |
|
|
| `/library` | `GrrLibraryView` | All, Media, Apps, Files; sort, filter, upload; the file viewer |
|
|
| `/projects` | `GrrProjectsView` | Projects, filter and sort, new project |
|
|
| `/project/:id` | `GrrProjectView` | A project's chats, rename, star, instructions, delete |
|
|
| `/search` | `GrrSearchView` | Messages, bots, group chats, files, routines |
|
|
| `/settings` | `GrrSettingsView` | The settings tree |
|
|
| `/settings/account` | `GrrAccountView` | Accounts, switch, add, sign out, delete account |
|
|
| `/settings/usage` | `GrrUsageView` | Weekly usage, days, gateway, model health, counters, change limit |
|
|
| `/settings/rules` | `GrrRulesView` | Auto-review rules |
|
|
| `/settings/rule/:id` | `GrrRuleView` | One rule (`new` adds), allow or ask first |
|
|
| `/settings/appearance` | `GrrAppearanceView` | System, Day, Night, Black, Blue, size |
|
|
| `/settings/language` | `GrrLanguageView` | Language |
|
|
| `/settings/haptics` | `GrrHapticsView` | The haptics switch (kept in the account; a terminal cannot vibrate) |
|
|
| `/settings/widget` | `GrrWidgetView` | The widget page: switch, open, copy the address |
|
|
| `/settings/computer` | `GrrComputersView` | Each bot computer: disk, update, reset |
|
|
| `/settings/connectors` | `GrrConnectorsView` | Tool groups on and off |
|
|
| `/settings/advanced` | `GrrAdvancedView` | Default tier, replace the key, export, server health |
|
|
| `/settings/builder` | `GrrBuilderView` | Builder state and settings (admins) |
|
|
| `/settings/feedback` | `GrrFeedbackView` | Send feedback |
|
|
| `/settings/timezone` | `GrrTimezoneView` | Time zone list with filter |
|
|
| `/template/:id` | `GrrTemplateView` | A bot template: details, publish, unpublish, copy link |
|
|
| `/template/:id/:part` | `GrrTemplatePartView` | Instructions, memories or skills of a template |
|
|
| `/public/:slug` | `GrrPublicTemplateView` | A shared template and Add to my bots |
|
|
|
|
## The chat
|
|
|
|
Messages are items in a page that follows the newest row while you are at the bottom (End jumps there, a hint shows
|
|
when something arrived above). Your messages are right-aligned bubbles, a bot's are left, rendered with the markdown
|
|
renderer of the chat scene (headings, bold, inline and fenced code, lists, quotes). A divider names the time when more
|
|
than 30 minutes passed, `NEW` marks what you have not read. Under the newest bot message, and under any message you
|
|
select, sit copy, good, bad and try again; steps fold open with Enter.
|
|
|
|
A pending approval is also asked at once as a dialog (Deny, Allow once, Always allow for the browser tool) the moment it arrives or the chat opens, without blocking anything: Esc leaves the card, a decision made elsewhere closes the dialog.
|
|
|
|
A running command that goes quiet on a prompt-like last row (a password, a `?`, `[y/N]`) opens its terminal by itself, so the prompt reaches the user and the answer is typed into the command; Esc leaves it.
|
|
|
|
Cards: approvals (Allow once, Deny, Always allow in browser for the browser tool, then the state), questions (lettered
|
|
options and Skip), files (Enter opens the viewer: text with line numbers, anything else is saved and opened by the
|
|
desktop), several files, templates (publish, copy link, details), and live process cards. A running command's card
|
|
follows the server's byte stream into a VT screen (8 rows, 24 expanded) with `^C`, Stop, Type (a full terminal in a
|
|
dialog, Esc leaves) and Expand; a finished one shows the stored transcript.
|
|
|
|
The composer is a multiline box (Enter sends, Shift+Enter or Alt+Enter adds a line, `@name` completes bots, PageUp and
|
|
PageDown scroll the conversation). The button on its right is Stop while the bot works, Send when there is text and
|
|
the voice mark otherwise. Plus attaches a file by path, inserts a skill or opens Connectors. A general chat has the
|
|
Ask, Imagine and Build tabs, the tier chip and the private-chat switch; a private chat is deleted when you leave it.
|
|
Unsent text is kept per chat while the program runs (`GRR_DRAFT_KEEP` chats).
|
|
|
|
## Keys
|
|
|
|
Mouse works everywhere: click rows, buttons, tabs, chips and tiles, wheel scrolls.
|
|
|
|
| Key | What it does |
|
|
|---|---|
|
|
| `Esc` | back, or close the open dialog or menu |
|
|
| `Up Down` | move the selection |
|
|
| `Left Right` | move inside tabs, chips, tiles and card buttons |
|
|
| `Enter, Space` | open the selected row, press the selected button, flip the selected switch |
|
|
| `PageUp PageDown` | scroll the page or the conversation |
|
|
| `Home End` | first or last row, End jumps to the newest message |
|
|
| `Tab, Shift+Tab` | next or previous area |
|
|
| `Ctrl+N` | new chat |
|
|
| `Ctrl+F` | search |
|
|
| `Ctrl+G` | settings |
|
|
| `Ctrl+O, F10` | the More menu of the screen |
|
|
| `Ctrl+P` | the side pane of a bot chat |
|
|
| `Ctrl+]` | leave the terminal of a bot computer |
|
|
| `F1` | key help |
|
|
| `Ctrl+Q` | quit |
|
|
|
|
In the terminal of a bot computer every other key goes to the shell. Tab still moves focus (the framework's rule for
|
|
every terminal widget, see [tui.md](tui.md)), so shell tab completion is not available there.
|
|
|
|
## Agents driving the TUI
|
|
|
|
While `--tui` runs it listens on a unix socket, `<TAI_HOME>/tui.sock` (or `TAI_TUI_SOCK`), mode 0600, one JSON line in and
|
|
one out. Any tai agent on the machine then has six tools (hidden while no TUI runs): `tui_screen` (route, size, focus and
|
|
the screen text; `until` waits for text to appear), `tui_keys`, `tui_type`, `tui_click`, `tui_goto` and `tui_result`.
|
|
Every action runs as a job on the UI loop. With `async` the call returns a job id at once and `tui_result` collects it,
|
|
waiting up to `wait` seconds; without it the call waits for its own job. Keys are the framework's names (`down`, `enter`,
|
|
`escape`, `ctrl+p`, a character); clicks are screen cells. A second TUI does not take over the socket of the first.
|
|
|
|
## Events
|
|
|
|
The server sends `bot.update`, `bot.delete`, `chat.update` and `chat.delete` (the lists are read again), `status`
|
|
(who is working on what, drives the working row, the busy dots and the stop button), `toast` (a banner with the bot
|
|
and a line, click opens the chat, not shown for the chat you are looking at) and these, handed to the open screen:
|
|
`message.new`, `message.update`, `routine.update`, `automation.update`, `library.update`, `usage.update`,
|
|
`builder.update`, `files.changed`, `port.opened`, `port.closed`, `preview.ready`, `preview.failed`, `chat.draft`,
|
|
`connector.update`.
|
|
|
|
## Character shapes
|
|
|
|
A bot's mark is two rows of seven cells (five when small, four rows as a hero) with two eyes, one template per shape:
|
|
`blob`, `cloud`, `tilt`, `plus`, `flower`, `leaf`, `pebble`, `oval`, `play`, `gem`, `cog`, `hex`, in the bot's own color.
|
|
|
|
## Caps
|
|
|
|
Every limit is a constant in `tai.py`.
|
|
|
|
| Constant | Value | Meaning |
|
|
|---|---|---|
|
|
| `GRR_HTTP_TIMEOUT` | 30 | seconds for one REST call |
|
|
| `GRR_SSE_TIMEOUT` | 60 | seconds of silence before the event stream reconnects (the server beats every 15) |
|
|
| `GRR_SSE_RETRY` | 2.0 | seconds between reconnects |
|
|
| `GRR_SSE_LINE_MAX` | 1048576 | bytes of one event line; longer ends the connection, it is counted and never parsed |
|
|
| `GRR_BODY_MAX` | 33554432 | bytes of one answer or download |
|
|
| `GRR_SIDE_W` | 34 | columns of the drawer in two-pane mode |
|
|
| `GRR_SIDE_MIN_COLS` | 100 | columns from which two panes show |
|
|
| `GRR_PANE_MIN_COLS` | 150 | columns from which the side pane sits beside the chat |
|
|
| `GRR_PANE_MIN_W` | 40 | narrowest side pane |
|
|
| `GRR_PANE_MAX_W` | 90 | widest side pane |
|
|
| `GRR_MESSAGE_KEEP` | 400 | messages a chat keeps in memory (older ones load again on demand) |
|
|
| `GRR_TOAST_SECS` | 3.2 | seconds a toast stays |
|
|
| `GRR_BANNER_SECS` | 5.2 | seconds a bot banner stays |
|
|
| `GRR_TOAST_MAX` | 4 | toasts at once |
|
|
| `GRR_COMPOSER_MAX_ROWS` | 6 | rows the composer grows to |
|
|
| `GRR_VIEWER_MAX_BYTES` | 2097152 | bytes the file viewer reads |
|
|
| `GRR_DRAFT_KEEP` | 64 | chats with a kept draft |
|
|
| `GRR_PROMPT_QUIET` | 0.8 | seconds of silence on a prompt-like last row before a running command opens its terminal |
|
|
| `GRR_PROMPT_ASKS` | 20 | times one process card opens its terminal by itself |
|
|
| `GRR_PROMPT_MAX` | 8 | pending approvals asked at once as dialogs in one chat |
|
|
| `GRR_LIST_MORE` | 12 | rows Show more adds |
|
|
| `GRR_MENU_MAX_H` | 14 | rows of a menu before it scrolls |
|
|
| `GRR_CONTROL_JOBS` | 64 | control jobs kept (finished ones drop oldest first) |
|
|
| `GRR_CONTROL_LINE` | 65536 | bytes of one control request |
|
|
| `GRR_CONTROL_WAIT` | 10 | seconds a control call waits for its job by default |
|
|
| `GRR_CONTROL_WAIT_MAX` | 60 | longest wait a control call may ask for |
|
|
| `GRR_CONTROL_KEYS_MAX` | 200 | keys in one `tui_keys` call |
|
|
|
|
## Measured
|
|
|
|
One machine (Linux 7.0, Python 3.14), headless harness with the real server on loopback, no terminal in the loop, median of
|
|
50 runs of "change, lay out, build the frame bytes":
|
|
|
|
| What | Result |
|
|
|---|---|
|
|
| boot to first frame (spec, locales, me, bots and chats over REST) | 29 ms |
|
|
| repaint of a 400 message chat, every row cold | 44 ms |
|
|
| scroll by 3 rows in that chat | 1.1 ms |
|
|
| one typed character in the composer | 0.12 ms |
|
|
| one new message appended | 2.0 ms |
|
|
| the settings page repainted | 0.8 ms |
|
|
| peak memory of that process | 44 MB |
|
|
|
|
Through the real binary (`tests/grr_tui.py`, a real grr server on a throwaway home, the real gateway behind it): typing a
|
|
message in the TUI, the server storing it, a real model answering and the answer reaching the screen took 11 s once; that
|
|
case is probabilistic by design (a free, rotating backend), the other three cases are deterministic and take 5 to 7 s each.
|
|
|
|
## Gaps, next to what closes them
|
|
|
|
- **Voice conversation and dictation** (the wave and mic buttons): no audio capture in a terminal client. The button
|
|
says so. Closes in `GrrChatView.send_action` with `tai.py`'s own `listen` and the server's `/api/voice/*` routes.
|
|
- **The graphical bot screen** (VNC) and **camera**: a terminal cannot draw either. `GrrScreenView` starts and stops
|
|
the screen and opens it in the desktop browser; the bot's shell is `GrrComputerView`.
|
|
- **Images** (link thumbnails, the image viewer, library thumbnails) are not drawn: they show as named files and open
|
|
in the desktop viewer. Closes in `GrrFileViewer` and `GrrMessageItem` with a pixel protocol (kitty or sixel).
|
|
- **Preview frame**: a terminal cannot frame a page, `GrrPanePreview` shows the address and opens it in the browser.
|
|
- **File highlighting** in the Files tab: line numbers only, no syntax colors (the web app uses the rule sets in the
|
|
server's spec). Closes in `GrrPaneFiles.view_file`.
|
|
- **Dates and relative times** use the system locale's day and month names and English words for "ago".
|
|
- **Settings that only a screen has**: the size slider is kept in the account but does not change the terminal; haptics
|
|
likewise.
|
|
- **Server-side chat drafts** (`chat.draft`) are not read; drafts are kept per run in memory.
|
|
- **Wide characters** count as one column (see tui.md), so CJK names in the language list can misalign by a cell.
|
|
- **Not measured**: frame cost of the grr screens on real terminals, memory over a long session, behavior on terminals
|
|
without truecolor (colors are sent as 24-bit values).
|