15 KiB
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.
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 (systemfollowsLC_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,statusand the other events listed under Events. It reconnects everyGRR_SSE_RETRYseconds 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.
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), 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_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_actionwithtai.py's ownlistenand the server's/api/voice/*routes. - The graphical bot screen (VNC) and camera: a terminal cannot draw either.
GrrScreenViewstarts and stops the screen and opens it in the desktop browser; the bot's shell isGrrComputerView. - Images (link thumbnails, the image viewer, library thumbnails) are not drawn: they show as named files and open
in the desktop viewer. Closes in
GrrFileViewerandGrrMessageItemwith a pixel protocol (kitty or sixel). - Preview frame: a terminal cannot frame a page,
GrrPanePreviewshows 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).