Both assertions only matched X.Y.Z, so the release gate (which now runs the TUI tests) failed on 1.0.1-rc.1. They accept a semver prerelease suffix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qqq ui: the QQQ terminal UI
A classic full-screen terminal UI for QQQ applications: function-key bar, PgUp/PgDn everywhere, Esc back,
a : command line that speaks the CLI grammar, and a live footer. OpenTUI 0.8.1 (React 19) on Bun 1.4.4.
Everything that is not drawing comes from the client kernel (@qrun-io/qqq-core): sign-ins and profiles,
the metadata cache, the filter grammar and its completion, FormSession, runProcess, the widget render
model, the record-view and association helpers, the result cursor, file and terminal checks.
Run it
qqq ui is bundled into every qqq release binary (curl installer, Homebrew, Scoop); qqq --version
says binary with ui. The npm package @qrun-io/qqq-cli does not carry it. From a source checkout,
node packages/cli/dist/main.js ui starts this package with Bun (QQQ_BUN names another bun):
npm ci && npm run build # once, at the repo root (the kernel and CLI are used built)
(cd packages/tui && bun install --frozen-lockfile) # once
qqq ui # the splash, then the only profile (or a picker); or: qqq --profile sample ui
qqq ui table query person -w "lastName STARTS_WITH S" # open a screen directly (CLI grammar)
qqq ui --theme light | --ascii | --linear | --no-mouse | --no-splash | --check | --no-check
bun run packages/tui/src/main.tsx --profile sample # the same without the CLI
Options: --profile|-p <name>, --server|-s <url> (a profile named after the host is made), --theme <name>, --ascii (ASCII glyphs), --linear (one region, no Apps pane), --no-mouse / --mouse (the
mouse for this run; the saved Mouse setting is on by default, see Mouse), --no-splash /
--splash (the start-up splash for this run; see Starting),
--check (the full terminal check, then exit), --no-check (skip the quick check at startup). Anything
after the options is a route in the CLI grammar.
Environment: QQQ_UI_LINEAR=1, QQQ_UI_ASCII=1 (as the options); QQQ_UI_SIGN_IN=browser|device (skip the
OAUTH2 choice); QQQ_UI_NO_BROWSER=1 (show the sign-in address without opening a browser);
QQQ_PROFILE, QQQ_SERVER, QQQ_TOKEN (a bearer token instead of the stored sign-in), QQQ_CONFIG_DIR,
QQQ_STATE_DIR, QQQ_CACHE_DIR, QQQ_KEYRING=file (keep sign-ins in a file instead of the OS keychain),
shared with the CLI. QQQ_UI_NO_SPLASH=1 (as --no-splash). QQQ_UI_NO_QUIT=1 is for the soak test only.
Starting
qqq ui opens on a splash: Query (the qrun.io mascot) blinking, his tail wagging while it works, the
qqq wordmark, "Terminal client for QQQ", the version (qqq 1.0.0 · UI API client 1.8.0) and one status
line that follows the boot: reading profiles, connecting to the server, signing in, loading metadata (with
the table count). The large Query needs about 34 rows; smaller terminals (80×24) get the small Query beside
the wordmark, very small ones the text alone; --ascii draws it all in plain characters. Any key stops the
animation (not the boot). The splash stays at least 600 ms so it never just flashes, then Home opens as
before. --no-splash, QQQ_UI_NO_SPLASH=1, or F9 › Settings › Splash on start: off (:splash off,
saved as ui.splash: off) skip it.
Which server:
--profile,QQQ_PROFILE,--serverorQQQ_SERVERnames one: it connects straight away. So does a config with exactly one profile.- Several profiles and none named: the Servers picker opens over the splash (the rows of the Servers
dialog, Servers: name, short name, server, status, last used; who is signed in, the sign-in,
the description), with the last used profile selected. Enter connects;
nadds a profile,eedits, typing filters; the status column fills in as the servers answer (the picker never waits for them). Esc quits and prints how to skip the picker (--profile NAMEorQQQ_PROFILE). - No profile at all (the first run): the add form opens directly; saving connects to the new profile. Esc
closes it; then
nopens it again and Esc quits.
The sign-in runs as before (a MOCK or anonymous session, the OAuth2 browser or device flow with the address
or code on the splash, the username and password dialog). A failure stays on the splash with the reason:
Enter (or r) retries, c opens the picker to choose another profile (Esc there returns), n adds one,
Esc quits. Ctrl+O opens the picker too. With --no-splash the sign-in screen comes first, as in 1.0.0,
and the picker or the add form opens over it in the same cases.
Keys
| Key | Does | Key | Does |
|---|---|---|---|
F1, ? |
keyboard reference for this screen: keys grouped by what they do, the screen's help text, and a pointer to /, F9 and : (no commands or places) |
Esc | back (the screen below keeps its cursor and filter) |
| F2 | Search: records across tables (quick search), the main way into the data; :search <text>; typing on Home starts it (see below) |
Tab | Apps pane / content |
/, ., Ctrl+K |
find anything: apps, tables, processes, widgets, reports, records, commands and help, grouped, best match first; :find <text> |
||
| F3, Enter | view, open | ↑ ↓ PgUp PgDn Home End | move and page |
F4, e |
edit | Ctrl+F, Ctrl+B | page forward and back (always works) |
F5, r |
refresh | Space, Shift+Space | page lists (on a grid Space selects) |
F6, f |
filter: condition rows (field, operator, value) with a live match count; F2 types the grammar | ← → | columns, tree |
F7, n |
new record (on a child list: a child of this record) | s |
sort by the column under the cursor |
F8, d |
delete, after a question | ] [, PgDn PgUp (Ctrl+N: next) in a record |
next and previous record of the list it came from |
| F9 | the menu in two levels: the areas (Context, Go to, Work, Connection, Settings, Help), then an area's entries; places and records are found with / |
b |
run a table process on the selection |
F10, :quit |
Quit, Reset (home screen, metadata refreshed, session kept) or Cancel; :reset and :home reset |
x, g, v |
export, go to by key, view settings |
: |
command line (history ↑↓, Ctrl+R reverse search, Tab completes) | Ctrl+S | save a form, continue a process step |
Ctrl+O, :servers |
Servers: switch to another saved profile, add, edit | ||
| Ctrl+C | quit at once (the terminal is restored) | c |
copy a record |
a |
actions here (the F9 Context entries) | A, D |
audits, developer view (record or grid) |
t |
tags of a record (collaboration extension) | Esc then 1..0, Ctrl+1..0 | F1..F10 when the terminal takes the F-keys |
Ctrl+P, p, PrtSc |
print what you see: record sheet, table list (all rows or this page), or the screen (printing extension; else save as a file) |
Shift bank (Shift+F1…F10)
A second row of ten function keys, like Norton Commander's Shift bar. Keys that do not apply on the screen are dimmed; pressing one says why.
| Key | Label | Does |
|---|---|---|
| Shift+F1 | Keys | the keyboard reference (as ?) |
| Shift+F2 | Servers | the Servers dialog (as Ctrl+O) |
| Shift+F3 | SQL | the SQL Runner (sql-workbench extension) |
| Shift+F4 | Bulk | bulk edit, bulk delete and processes on the grid selection (as b) |
| Shift+F5 | Metadata | refresh the metadata from the server |
| Shift+F6 | Views | saved views of the grid |
| Shift+F7 | Copy | copy the record, or the grid row under the cursor, into a new record (as c) |
| Shift+F8 | Export | export the grid to a file (as x) |
| Shift+F9 | History | the History screen (as :history) |
| Shift+F10 | Assistant | show or hide the Assistant panel (the same action as F12 and Ctrl+G; assistant extension) |
The bar shows the Shift row under the primary one (prefixed ⇧) when the terminal has 30 lines or more.
:fkeys one|two|auto or F9 › Settings › F-key bar chooses: auto (two rows from 30 lines), two rows
always, or one row, where the Shift row takes the primary row's place for 2 seconds after any Shift+F
press (a terminal cannot report Shift being pressed and released on its own). A click on the Shift row runs
that key. The assignments are one table (src/keys/shift-bank.ts), each naming an action in
src/keys/actions.ts.
What terminals send for Shift+F1…F12 is decoded in one place (src/keys/fkeys.ts):
| Terminal | Shift+F1…F12 | Notes |
|---|---|---|
| tmux (any outer terminal) | ESC [1;2P … ESC [1;2S, ESC [15;2~ … ESC [24;2~ |
verified live; tmux re-encodes the keys it knows |
| iTerm2, Windows Terminal, xterm, VTE, Konsole, Alacritty | the same xterm forms | kitty-protocol terminals (kitty, WezTerm, Ghostty, iTerm2 with CSI u) send ESC [13;2~ for F3 and may send ESC [57364;2u…; both read |
| macOS Terminal.app | Shift+F5…F12 are ESC [25~ … ESC [34~ |
Shift+F1…F4 have no code of their own by default: add them in Settings › Profiles › Keyboard (\033[1;2P …) or use the F9 menu |
| rxvt, urxvt, Linux console | Shift+F3…F10 are ESC [25~ … ESC [34~, Shift+F11/F12 ESC [23$ / ESC [24$ |
Shift+F1/F2 send the same as F11/F12 there |
Shift+F3 in the xterm form (ESC [1;2R) looks like a cursor position report, which OpenTUI drops; it and
the legacy forms are rewritten on the input stream into the canonical ESC [n;2~ (never in the first
1.5 s after start or a resume, while the terminal answers OpenTUI's cursor queries). The legacy numbering is
picked from TERM_PROGRAM and TERM; QQQ_FKEYS=vt220|rxvt|apple|modern overrides it (vt220: ESC [25~
is Shift+F1; modern: legacy numbers are ignored, the default in tmux, iTerm2, Windows Terminal and kitty).
Ctrl+F1…F12 are decoded too but have no bindings yet: Ctrl+F5 acts as F5.
Keys on one screen:
| Screen | Keys |
|---|---|
| Grid | q quick search, S column statistics (from the loaded rows when the server offers none; a there loads up to 10,000 rows and recomputes), l follow the link in the cell, Ctrl+A select (page, whole result, first N), Space select row, b run on the selection, a bulk actions and processes |
| Record | l open the row under the cursor, Z share (shareable tables), t tags, A audits, D developer, c copy; with the collaboration extension C comments, P attachments, K tasks, T timeline, W follow / stop following |
| Comments, attachments, tasks | n adds (a comment, an upload from the file navigator, a task); Enter replies, downloads (file navigator) or deletes, marks a task done or open |
: output |
J switches an extension operation's JSON between rows and sub-tables and the raw JSON |
| Form | Tab and ↑↓ move, Enter or typing opens a picker, Ctrl+S save, Esc asks before dropping changes |
| Process | Ctrl+S continue, F4 back (when the step allows), Esc cancel, w save the step's file, n p page the records |
| Bulk wizard | Space switch a field on (bulk edit), Enter choose, h header row, l layout, k key fields, P saved profiles, v validate, Ctrl+S next, F4 back |
| Widget | Enter on its heading: reload, dropdowns, export CSV, run, versions, setup editors |
Developer (D) |
←→ or 1-9 sections, ↑↓ PgUp PgDn scroll, Enter follow the line, v view the value in full, y copy it, F3 the record (or the grid), Esc back; see Developer view |
| Command line | ↑↓ history, Ctrl+R reverse search (as bash: type to narrow, Ctrl+R or ↑ older, ↓ newer, Enter runs, Esc or ←→ edits, Ctrl+G cancels; searches the activity log's commands, CLI ones too), Tab complete, Enter run, Esc close |
History (:history) |
type to filter, Enter does the entry again, y copies the CLI equivalent, c or F8 clears (asks), t relative or absolute times, / starts a filter beginning with y or c, Esc clears the filter, then back |
| Filter (F6) | ↑↓ rows, ←→ Tab cells, Enter or typing edits the focused cell (text and numbers in place, choices from a list, Space cycles true/false), Backspace clears it to (any), Ctrl+N add, Ctrl+D remove, Ctrl+O AND/OR, Ctrl+G group, F2 Advanced (grammar, cheat sheet, caret under errors), F10 apply, Esc cancel; see docs/filter-dialog.md |
Printing: Ctrl+P prints what you see, on every screen (also p on records and grids, Print… in a
and F9, F8 where the screen has no Delete for it, :print, and the Print Screen key where the terminal
passes it on — most do not, so Ctrl+P is the key to rely on):
- a record view prints its record sheet (the record's sections in tier order and a summary of each child list);
- a grid prints a table list with the columns, widths, filter and sort you see. It asks what: all N rows (current filter and sort) — the first choice when the grid is filtered or has at most 2,000 rows — or this page, and with a Space selection the selected rows as a list or a record sheet for each selected row. The server prints up to 10,000 rows (more prints the first 10,000 and says so), repeats the header on every page, turns the page landscape when the columns need it and puts the table, filter, sort, row count, user and time in the header and footer;
- any other screen (home, dashboards, the developer view, SQL results, process results, Search, History, output) first asks Print what?: Content (the default) prints the screen's panels as a document — each panel a section under its title, no Apps sidebar, title bar, key lines or borders, wrapped to the page — and Screen prints the terminal as drawn (monospace, sized so a wide terminal keeps its lines).
Every page carries the app's header (the instance's name, the screen's breadcrumb such as People › Person › Ada Lovelace, the server, you, and the time in your date format) and footer (qqq ui 1.0.1 · <server>, a grid's filter and sort, page n of m).
Then the printer picker, with Save as PDF… first: it renders the same document on the server and saves
it the way downloads and exports are saved — the save dialog (or, with download-mode silent, straight to
the download folder), the stamped name (Customer-Ada-Lovelace_2026-11-01_120301.pdf), never overwriting
without asking. :print pdf saves without the picker. Then printers that can print now (approved, exposed by a connected print agent, PDF)
come first, the others are listed with the reason and cannot be picked; the printer chosen is remembered
per profile and preselected next time, marked "(last used)". :print <printer> skips the picker (name,
unique prefix or id). The title bar and status line show print 1/3 RECEIVED while jobs run, and a flash
says when they have printed (or which state stopped them, or the row-limit note). :printjobs (F9 › Work ›
Print jobs) lists recent jobs. Without the printing extension (or on a server older than printing 2.1 for
lists and screens) Ctrl+P saves instead: a grid opens its export, other screens save their text as a file.
The CLI forms still work on the command line: :print person 1 2, :print job list. Record sheets and
table lists need qbit-printing 2.1 (registerAll puts Print on every table).
Single-key shortcuts follow the server preference keyboard.singleKeyShortcuts and can be switched in F9
Settings; every F-key action is also in the F9 menu and on the : line.
Mouse
The mouse works everywhere and adds to the keys; nothing needs it, and every click does what a key does. It follows the classic full-screen conventions (Midnight Commander, Turbo Vision):
| Where | Click | Double-click | Right click | Wheel | Drag |
|---|---|---|---|---|---|
| Grid | row and cell under the cursor | opens the record | the actions (a) at that row |
3 rows; Shift+wheel 3 columns | header: at a column's right edge resizes it, elsewhere moves it (saved as the view settings save widths and order) |
| Apps tree | selects; on ▸/▾ opens or closes | opens or closes a node, opens a table, process or app | 3 lines | ||
Lists (home panels, app home, record child lists, : results, sign-in) |
selects | runs the entry (Enter) | the actions there | 3 lines | |
| Record, form, process, bulk wizard | focuses the field or line | opens a choice list or file dialog, toggles a check box | the actions | 3 lines | |
| Developer view | a section tab switches to it; a line selects it | follows the line (Enter) | 3 lines | ||
| SQL Runner | focuses the editor (and puts the caret there), the results or the schema; a result row and cell; a notebook cell tab; the g grid c chart p plan tabs |
whole value of a result cell; inserts a schema name | 3 lines; Shift+wheel result columns | ||
Completion popups (SQL editor, : line) |
selects a line | inserts it (Tab) | moves the choice | ||
| Date picker | picks the day; ◂ ▸ page the month | picks the day and accepts | moves by weeks | ||
Dialogs (F9 menu, Servers, / search, F1 keys, pickers, files, tags, view settings, filter, notebooks, cells, history) |
selects an entry or cell; [ Yes ] [ No ] [ Apply ] [ Cancel ] answer; in F9 the Menu › Area line goes back a level |
opens an F9 area, runs or picks (Enter); switches to a server | scrolls the dialog | ||
| F-key bar | presses that key (also with a dialog open, as the key would) | ||||
| Footer | the server and profile open the Servers dialog (Ctrl+O) | ||||
| Screen saver, lock | wakes it, as a key does (then the PIN, when one is set) |
A click on the screen or the Apps pane focuses it; a click beside a dialog does nothing (it does not close it); hovering does nothing. The terminal reports presses, drags and the wheel only (button-event tracking with SGR 1006 coordinates, which iTerm2, Terminal.app, Windows Terminal, kitty, WezTerm and tmux all speak); there is no motion tracking.
Selecting text. While the TUI has the mouse, the terminal selects text only with a modifier: Shift+drag
in most terminals (Windows Terminal, tmux, kitty, most Linux terminals), Option/Alt+drag in some (iTerm2);
Terminal.app toggles mouse reporting with View › Allow Mouse Reporting (⌘R). Help (?) and the dialogs
that show text to copy say so in their footer. F9 › Settings › Mouse: off (or :mouse off, saved in ui.mouse; --no-mouse for one run)
gives the terminal plain selection back; :mouse on or Settings again turns it on.
tmux. With set -g mouse on, tmux passes clicks and the wheel to the TUI while it has the mouse (and
selects text with Shift+drag); with mouse off, tmux sends nothing and the TUI is keyboard-only. In tmux's own
copy-mode the wheel scrolls tmux's history, as usual.
Developer view
D on a record opens its developer view, D on a grid the table's. Each is a row of sections (←→ or
the digits switch) over a scrollable pane with a scroll bar and a "lines a–b of n" counter:
- Record: Security (record security locks), Events (CloudEvents attributes, where the table
publishes, its subscribers, a sample event), Scripts (associated scripts and their revisions:
Enter shows the code, the run logs or test-runs a revision; a line stores a new version from
$EDITOR), JSON (API endpoints, each field's display and raw value, the raw values), Metadata (summary, fields, sections), Audits (Enter shows an entry's changes), APIs (application APIs; Enter on a version opens its OpenAPI document), then one section per further developer panel the server adds (Quick Search, SQL Workbench, Assistant...). - Table: Events (publications and subscribers with their message counters; Enter on a row shows every counter), JSON (the full table metadata), Metadata, APIs.
Tables use the grid's column layout (cells cut with …, — for no value, with a line saying why when
a whole column is empty). JSON is pretty-printed and tinted; a long line is cut in the pane, and v
opens the value viewer, which wraps it in full. y copies the selected value (pbcopy, wl-copy, xclip or
xsel when present, else the terminal's OSC 52). Links of the web UI become TUI actions (the table
developer view, a record, a grid, a process); those with no TUI screen, such as the ESB overview, show
their URL marked "browser only". Design notes: docs/developer-view.md.
The F9 menu
Two levels, as a menu bar. Level 1 lists the areas with a count and what each holds:
| Area | Holds |
|---|---|
| Context | the actions of the screen you are on (record, grid, process, form, app), and Back |
| Go to | Reset (home), search (/), the apps and the recent records (groups: five and more…), favorites |
| Work | jobs, print jobs, history, notifications, my work, extensions |
| Connection | the server in use (short name and host), Servers (Ctrl+O), profile, stream, metadata, sign out |
| Settings | theme, view settings, shortcuts, layout, ASCII, lock, PIN, idle timeout, folders, server preferences |
| Help | keys and help, search help, check the terminal, help-authoring mode, about, quit |
↑↓ moves, Enter or → opens an area, a letter jumps to the area starting with it (again: the next one, so
c c is Connection). In an area: ↑↓ and Enter run an entry (its key is on the right), ← or Esc go back
to the areas, Tab and Shift+Tab step to the next and previous area, a letter jumps to the entry starting
with it. Go to lists the apps and the recent records as groups; a group of more than 5 entries (any entry
can name its group) shows its first 5 and more…, which opens the whole group.
Two or more typed characters search every entry of every area (labels, details, : commands, keys;
/ is the search over tables, processes, widgets and records) and list the matches by area; Tab jumps between areas, Backspace back to nothing
returns to the areas, Esc or F9 closes. An entry that opens a dialog leaves the menu underneath and comes
back to the same entry; an entry that navigates closes it. The menu fits 80×24.
Servers
Ctrl+O (also :servers, F9 › Connection › Servers) opens the saved profiles, one per two lines:
Name Short Server Status Last used
▶*● sample sample http://localhost:8077 UI API 1.8.0 just now
Tester · mock session · QQQ sample server
enterprise ent http://localhost:8078 UI API 1.9.0 2 h ago
Admin · mock session · Enterprise demo: 22 apps, 182 tables
* is the default profile, ● the one in use. Status is probed in the background (GET /qqq/v1/version
and the authentication metadata, 2.5 s timeout): … while probing, the UI API version, or unreachable.
The second line is who is signed in (the keyring's sign-in, else who signed in last), the sign-in, and
the description.
| Key | Does |
|---|---|
| ↑↓ PgUp PgDn | move |
| Enter | switch: the stored sign-in is reused when it is still valid, else the sign-in runs; Home opens on that server with its metadata fetched again |
n, F7 |
add a profile: name, short name, description, server, sign-in mode (auto, browser, device); saving checks the server answers /qqq/v1/version (Ctrl+S again saves an unreachable one anyway) |
e, F4 |
edit the profile (a new name keeps its sign-in; a new server drops it) |
d, F8 |
remove the profile and its sign-in, after a question (not the one in use) |
* |
make it the default (what qqq and qqq ui use without --profile) |
r, F5 |
probe every server again |
| typing | filter on name, short name, description and server; / first to filter by a word starting with n, e, d or r |
| Esc | clear the filter, then close |
The short name (2 to 6 characters) is what the footer's connection segment (localhost:8077 · sample · mock session), the Home header and the F9 Connection line show; without one it is the name up to 6
characters, else the initials of a name in parts (acme-prod-eu › ape) or its first 3 characters
(enterprise › ent). The CLI edits the same file: qqq profile list [--probe], qqq profile add <name> <url> [--short s] [--description d] [--auth-mode m] [--check] [--default], qqq profile edit <name> [--server url] [--short s] [--description d] [--auth-mode m] [--rename new], qqq profile use <name>,
qqq profile remove <name> (also as qqq config profile …).
Screens
Home is the landing page (and the target of F10 › Reset): greeting and connection, counts, recently viewed
records, quick actions (runnable processes, new records, favorites with collaboration), application cards,
work (jobs, alerts, my work, release notes, extensions) when present, and the instance home app's widgets
when the metadata declares homeAppName. Tab moves between panels; v orders them.
Sign-in (profiles; MOCK and FULLY_ANONYMOUS get a server session; TABLE_BASED asks for a username and a
masked password, sent once and never stored; OAUTH2 signs in in the browser with authorization code + PKCE
on a loopback redirect, or with the device flow; AUTH_0 uses the browser with the API audience and keeps the
server session; a 401 mid-use opens sign-in on top and returns to the same screen; sign-out can end the
provider session too; the sign-in is shared with the CLI), home (apps with counts, recently viewed), app home
(widgets drawn from render blocks: stats, bar charts in block characters, sparklines, percentage bars,
tables, steppers, then sections with record counts), table grid (server paging, sort, columns that use the
width, horizontal scroll, selection with Space and a bulk bar, export, go to by key), record view (sections
by tier, links, child lists, next and previous through the list it came from), create/edit/copy forms
(editors per type, a picker that narrows for contingent fields, local and server validation by field,
DATE_TIME in the local zone), the process wizard (each step's form, validation review, progress with N/M
and elapsed, summary, cancel; ad hoc widget inputs and buttons), and the output pane of : commands run
through the CLI in-process.
Record view: the sections in tier order, then the child lists. The layout is the same for every record of
a table, so paging with PgDn/PgUp moves nothing: field columns are placed from the metadata, and each child
list is a box of fixed height (heading, column header, 5 data rows, a count line). A shorter list pads with
blank rows, an empty one still shows its box, and a longer one scrolls inside the box ("rows 1-5 of 12"):
on a list, PgDn/PgUp scroll it a box at a time and Down/Up (j/k) a row at its edges, while ] [
still step records; Enter opens the child under the cursor. A section whose childRecordList widget lists
an association (by manageAssociationName or the association's joinName, as the Next UI binds it) shows
that association there, once, under the section's label and with the widget's columns; other associations
follow the sections, and a child's own child lists come last. The record view settings (v) are a form
like the grid's View settings, saved per table in ui.views.record:<table>: Columns (auto, 1, 2 with Space or
←→), Label width (labelWidth, 8 to 60: type digits, +/- or ←→ step by one, Backspace returns to auto),
Child list rows (childRows, 1 to 50, the same keys; Backspace returns to the default, ui.childRows of the
profile, else the global ui.childRows, else 5), the Collaboration panel when the extension is present, and
Reset to defaults (r, with a confirm). * marks a value changed from the default; Enter saves, Esc cancels.
Nothing is drawn outside its container: every line is cut to the cells it has (measured in terminal cells,
so wide characters count double) and a cut ends in …; dialogs never extend past the screen, and key help
lines break between keys (at ·), dropping the least important keys first when they do not fit. Long
values in grid cells and record fields are cut with … to the column; V on the grid (or "Whole value" in
a / F9) shows the whole value of the cell under the cursor, wrapped in a dialog, and V on a field row of
the record view does the same for its field (both fields of a two-column row). A column width saved wider than the terminal is held to the grid's width.
Widgets (home, app homes, record sections) have their actions on Enter: reload, the payload's dropdowns (possible values or a date; kept per widget when it asks), export CSV, run a process widget, data bag versions, script revisions, and on a saved report the filter/columns and pivot setup editors (API-versioned setups go through the application API). Denied widgets are not requested; a parent widget loads its children in a grid or one tab at a time. A dynamicForm section has "Edit these fields".
Search (F2, everywhere; on Home the search line is first and typing starts it): a search line with
results as you type, grouped by table with the table's label and count; each hit shows the record label,
its key and the highlighted snippet with the matched field. ↑↓ move, ←→ or Tab go to the next or previous
table, Enter opens the record (Esc comes back to the same hit), o opens the table's grid filtered to the
hits, Space marks hits and a offers the grid (export, print, bulk there) and the table's processes for
them, f searches one table, h the recent searches of the profile, ? the quick search syntax. The status
line reads "N matches in M tables · 42 ms · index 1,234 records · last reindex 10:12" (from the quick search
status). Quick search matches every word (AND) from the start of words, case-insensitively; it has no
phrases, field:value, wildcards or OR (status:new searches new, with a note). Without the quick search
extension F2 still opens Search, finding tables, apps and processes by name and the records of the table
it was opened from ("quick search not installed: searching names only").
Collaboration (collaboration extension) lives at the bottom of the record view, never above its
sections: the record starts right under its title and the cursor on its first field. A strip directly above
the key line shows the tags, the counts of comments, files and open tasks, the timeline, whether you follow
the record and who is viewing it (Tags: rush, vip (t) · 3 comments (C) · 1 file (P) · 0 tasks (K) · timeline (T) · following (W) · viewing: Admin (you)); when others are on the record their names and what
they do (Pat (editing email)) replace "(you)" and the strip takes an accent colour. When the strip does not
fit, timeline and the follow state go first, the viewers and the tags last. On terminals of 140 columns or
more a Collaboration panel on the right (36 columns) shows the tags, the latest two comments (author, age,
first line), the open tasks and the next due date, the files, the followers and everyone viewing now with
what they are doing; the strip is then the keys only. The panel is a record view setting (v:
Collaboration panel row: auto, on or off; on shows it whenever the record keeps 60 columns). Presence changes
redraw the strip and the panel live. Every strip segment and panel row is a click zone that presses its key
(t C P K T W; the panel's heading lists everything, a viewer shows who is here). a and F9 list
comments (list, add, reply), follow or stop following, attachments (list, download, upload, delete), the
timeline, tasks (list, add, done) and the share preview (find people or type user ids, see who could read
the record, then send). Without the extension neither the strip nor the panel is drawn and F9 shows
"Collaboration: needs the collaboration extension".
Text and HTML widgets are drawn as a document: headings (HTML h1-h4, markdown #, or a short line
before a block) in the accent colour with a blank line before, status badges (OK green, WARN/DEGRADED
yellow, FAIL/DOWN red) at a fixed width, Label: value lists aligned, dates in your date format (:dates),
paragraphs at ~100 cells, enumerations as bullets (src/text-doc.ts).
Widgets the kernel has no block for are drawn by the TUI: the ESB overview (destinations with their
publishers, triggers and counters, as :esb overview; Enter on a destination offers the ESB of its table
and processes, its messages and dead letters), statistics that link to a filtered grid (the label and
count; Enter opens the grid), row-shaped HTML (the alerts setup checklist) in aligned columns, and any
other payload as key/value rows and sub-tables. App cards show the app's Material icon as a glyph (an
ASCII stand-in under the ASCII setting; unknown icons get the app mark).
Dates and date-times use one control everywhere they are edited (forms, process forms, bulk edit, the
filter dialog's value cell). Typed entry shows the value in the profile's display format, with the format
hint (YYYY-MM-DD, MM/DD/YYYY hh:mm AM CST) dim on the right, and accepts:
| Typed | DATE | DATE_TIME |
|---|---|---|
2026-03-05, 2026-3-5, 2026/3/5, 20260305 |
2026-03-05 | 2026-03-05 00:00 |
2026-03-05T14:30, 2026-03-05 14:30:15, 2026-3-5 2:30pm |
refused (a date has no time) | that time |
2026-03-05T20:30:00Z, …+02:00 (an instant) |
refused | read in the local zone |
03/05/2026, 3/5, 3/5/26 (order MDY), 05/03/2026, 5.3 (order DMY) |
the date; the current year when left out | the date (and a time after it) |
t, today / n, now |
today | today 00:00 / now |
14:30, 2:30 pm (a time alone) |
refused | today at that time |
now(), now(-7d), now(+2h), startOf(month), startOf(month, -1) |
resolved to a date | resolved to a date and time |
| empty, or Backspace on a whole value (when the field may be empty) | no value | no value |
Slash dates are refused under the ISO order (01/02/2026 could be month/day or day/month): :dates order mdy or dmy says which. A date that does not exist is refused with the reason (February 2026 has 28 days, hour 25 is out of range (0-23)), shown in red beside the field, which keeps the focus. F2 or
Ctrl+Space opens the calendar under the field (above it when there is no room): ←→ a day, ↑↓ a week,
PgUp/PgDn a month, Shift+PgUp/PgDn a year, Home/End the first or last of the month, t today, n now, Tab to
the time row (hour, minute, seconds when the value has them, AM/PM on a 12-hour clock; digits or ↑↓, Tab or
←→ between parts), Enter accepts, Esc cancels, and a digit typed on the calendar goes back to typed entry.
For a DATE_TIME it shows the local zone, the UTC instant that is sent, and the server's zone when the
instance declares one. In the filter dialog Enter on a date value opens the calendar (one for from and one
for to with between) and p there opens the presets (now(-7d), thisPeriod(month) … kept as
expressions); typed values are read the same way. Enter on an empty date field opens the calendar when
:dates enter picker is set. Values go out as before: YYYY-MM-DD, and DATE_TIME as the UTC instant of the
local time.
The bulk wizard (a on a grid: bulk insert, bulk edit with a file, bulk edit, bulk delete, the upload
template) runs on the kernel's runBulkProcess: the file from the file navigator, the column mapping as a
grid (Enter chooses a column, a typed default or value mapping; h header row, l layout, k key fields,
P saved profiles, yours and shared), value mapping with the picker, the review with optional validation,
and the result. Shareable records (saved reports, saved views) have Share (Z, :share): add a user or
group Read-Only or Read and Edit, change, remove; only the owner may share. :helpkeys turns on
help-authoring mode (F1 shows every help slot key).
Assistant
With the assistant UI API extension (qbit-assistant), F12 shows and hides the Assistant panel on
every screen (also Ctrl+G, :ask [question], F9 › Work › Assistant, or a click on 12Ask). It is a
chat pane on the right of the screen (40 to 64 columns), or the whole body on terminals narrower than 110
columns. The question line is at the bottom and the conversation above it. Answers are read-only and use
your permissions.
Hiding the panel keeps the conversation, its context and the draft on the question line; the next F12 shows them again. When the panel is shown but the screen beside it has the focus (after opening a link), F12 goes back to the question line. F11 is reserved.
Answers are drawn from their markdown: entities decoded, headings in the accent colour, paragraphs
wrapped to the pane, bullet and numbered lists, bold and code, and pipe tables laid out like the
grid (columns that do not fit are named underneath, cells cut with …). A link shows as label ↗, never
its address; each is a Tab target in reading order, once per target. Records and tables the tool results
refer to that the text does not show are listed under →. Exact duplicate paragraphs are shown once;
the timing and token line stays, dimmed.
Questions carry the screen the panel was shown over as context, the way the Next UI's panel sends it: a
record (/app/{table}/{id}) or a grid with its filter; the first line of the panel says which
(on: Person #3). While the panel is beside the screen, a record or grid you open becomes the context.
| Key | Does |
|---|---|
| Enter | ask the question; on a link, open it (a record or a grid, beside the panel; a full-width panel hides so the screen shows) |
| Tab, Shift+Tab | move over the answer's links; typing returns to the question line |
| u | on a link: show its address |
| ↑ ↓ | the questions you asked on this profile (on the links: previous and next link) |
| PgUp PgDn, wheel | scroll the conversation |
| Ctrl+D | send without (or again with) the page context |
| Ctrl+L | a new conversation |
| Ctrl+X | stop the answer being written |
| F12, Ctrl+G | hide the panel (again: show it) |
| Esc | hide the panel (with text on the question line too: nothing is asked, the draft stays); on a link, back to the question line |
F12 in your terminal. The F-key bar shows 11 (blank, reserved) and 12Ask from 120 columns;
narrower, the ? help and F9 › Work say "F12 assistant". The TUI reads F12 both as the legacy sequence
(ESC [24~, also with modifiers, as tmux and iTerm2 send it by default) and as the kitty keyboard
protocol's key (57375). macOS may take F12 for Mission Control or Dashboard, or send it only with fn
(System Settings › Keyboard › Keyboard Shortcuts, or "Use F1, F2, etc. keys as standard function keys");
Ctrl+G is the fallback everywhere.
Without the extension the menu entry is disabled, 12Ask is dimmed and F12 says what is missing. When
the assistant is off for you (no model provider on the server, no permission, the kill switch) the panel
shows the server's reason and what to configure; F5 checks again. The command line has the same:
qqq assistant ask, qqq assistant config.
SQL Runner
With the sql-workbench UI API extension (qbit-sql-workbench), the SQL Runner is the terminal face of the
Next UI's Runner page (a SQL_WORKBENCH widget). Open it from the SQL Workbench app's Runner in the Apps
tree, / (type "sql"), F9 › Work › SQL Runner, or :sql (:sql SELECT … opens and runs a statement,
:sql open <id> opens a notebook). Without the extension the Runner app and the entries are hidden and
:sql says the extension is missing. The SQL Workbench app's home is the usual app dashboard (notebooks and
the execution audit), and its tables open as tables.
The layout is a classic database TUI: the SQL editor on top (line numbers, syntax tint, the server's
keywords), the result grid below drawn like the main grid (columns in the server's order, a row-number
gutter, a header rule, dates at seconds precision, grouped numbers right-aligned, NULL as a dim —, zebra
per the sql:results view setting), then a status line (rows, elapsed, backend, read-only, row limit,
sorted by) and the key line,
an error under the editor with its SQL position marked in the text, and the schema browser on the right.
| Key | Does |
|---|---|
| F5, Ctrl+Enter (Ctrl+J) | run the statement under the cursor (the whole cell when it has one); :name parameters are asked for, bound as values and remembered |
| F6 | EXPLAIN: the plan as an indented tree with cost, rows and a relative cost bar; ←→ or Enter fold, r the raw text |
| F3 | schema browser: backends › tables › columns, type to filter, → unfolds, Enter inserts the name (on a backend: run this cell there) |
| Tab | the completion popup, also with nothing typed: after FROM / JOIN every table (with its column count), after SELECT / WHERE / ON or alias. the statement's columns (with types), at the start the statement keywords and recent statements; typing filters, ↑↓ Enter or Tab inserts, Esc closes |
| Ctrl+↑ Ctrl+↓ | earlier and later SQL from your run history (kept on disk) |
| F2 | notebooks, Mine and Shared: open, s save (a new version), a save as, n new, u duplicate, d delete, p publish |
| F4 | cells: Enter edits one, a add SQL, m add notes, K J (Shift+↑↓) move, t title, b backend, d delete, R run all |
| F7 | your audited runs (status, time, rows, SQL); type to filter, Enter opens the SQL in the editor, Ctrl+R re-runs it |
| Shift+Tab, Esc | to the results and back; Esc also cancels a running statement and, from the editor, leaves (asking about unsaved changes) |
| results: ↑↓ PgUp PgDn ←→ | move; rows load page by page from the server's cached result |
F8, results: x |
the export dialog: csv, tsv, xlsx or json of every row of the result (the server writes csv/xlsx/json from its cached result; tsv is read page by page) through the file navigator and the download-folder preference (the name is pre-filled and editable, e.g. sql-result-1a2b3c4d_2026-11-01_102316.csv), or copy to the clipboard for small results |
results: s V y Y Ctrl+Y |
sort by the column (the server sorts: asc, desc, off), the whole value, copy the cell, the row (TSV), the whole result (TSV) |
results: Shift+←→, a |
scroll the columns (◂ ▸ show more off screen); the actions menu (export, copy, chart…) |
results: g c p |
grid, quick chart (text bars of a number column; ←→ value column, l label column), plan |
Markdown cells are edited as text and shown rendered below. Runs are read-only (one statement, the server's
row limit and timeout; every run is audited). The capabilities decide the backends offered, the row limit
shown and whether EXPLAIN ANALYZE is offered (it needs the write permission). scripts/smoke-sql.sh [server]
drives it in tmux at 80x24 and 120x36 against a server with the extension (default http://localhost:8078).
The : command line
TUI commands: home, back, quit, reset, refresh, help, helpkeys, menu, search <text> (also find, goto, palette), view,
views, share, actions, audits, dev, tags, report run <name>, theme <name>, ascii on|off,
linear on|off, shortcuts on|off, dates [order ymd|mdy|dmy] [clock 24h|12h] [week monday|sunday] [weeknumbers on|off] [enter picker|next], lock, saver [style|style <s>|refresh <s>|tables <a,b>|burnin on|off|dim <minutes|off>], pin, pin remove, idle <minutes>, filter [clear|<grammar>],
export <format>, jobs, history, profile [name], servers, server <url>, signout, connection, doctor, open <table>,
app <name>, record <table> <key>, sql [statement | open <id>]. Tab on an empty line lists every
command with a one-line description; after a command it lists its arguments (tables, processes, apps,
profiles, subcommands), in the same popup as the SQL editor. table query|get|create|update|delete and process run open screens;
any other CLI command runs in-process (runCli from @qrun-io/qqq-cli/program) and shows its output;
an extension operation (:alerts get-inbox, :collab get-records-tasks customer 1, :api <operationId>)
shows its JSON as key/value rows and sub-tables, with J for the raw JSON.
History
:history (or F9 › Work › History) opens everything done on this profile, in the TUI and with the CLI
(qqq history lists the same log): number, time, client, kind (100 columns and wider) and summary, newest
at the bottom with the cursor there; the line under the list shows the entry's command. Typing filters
(every word must match the summary, command, kind or client). Enter does the entry again: a : command
runs, a record, app or process opens, a query opens the table with its filter, a CLI command runs on the
: line (asking first when it changes data). y copies the CLI equivalent, c or F8 clears the history
after a question. The screen fits 80×24.
What the TUI records: : commands; opened apps, tables, records and processes (with the CLI equivalent,
e.g. qqq table get person 3); queries with filters (qqq table query person -g "…", values of password
fields as ***); records created, edited and deleted (which record and fields, never values); process
runs with their outcome and time; exports with the file; prints with the job ids; server switches; sign-in
and sign-out. Ctrl+R on the : line searches these commands too.
Screen saver and lock
:lock, F9 › Settings › Screen saver / lock, or the idle timeout (:idle <minutes>) shows the screen
saver; any key wakes it. With a PIN set (:pin) the PIN prompt opens over the saver, which keeps
animating; Esc goes back to the saver. :saver previews it at once (no PIN asked), :saver bounce,
starfield, flip or big previews one style.
Live tiles, refreshed every 10 to 30 s (:saver refresh <seconds>, F9 › Settings) and only while the
saver shows. A tile whose extension is missing or that answers 403/404 is hidden, so the saver works on
the plain sample server; one that fails shows …, then its last value dimmed as stale, and backs off
(interval × 2ⁿ, at most five minutes).
| Tile | Needs | Shows |
|---|---|---|
| Online | the session store, or collaboration | who is signed in: with the session store qbit, the sessions active in the last 10 minutes and their users ("3 sessions · 2 users (you ×2, bob)"; your own sessions count); otherwise you plus the people in presence across a few tables per refresh (favourites and recents first), who and on what. It never says "nobody around" while you are signed in. A server that stores no sessions (mock auth) or merges presence per user cannot tell two terminals of one user apart: both show "1 online · you". The saver keeps the presence lease of the screen under it, so others see you still viewing it, not gone |
| Jobs | durableJobs |
running, queued and recently failed jobs (all users with jobs_admin, else yours) |
| Health | health | the summary status and up / degraded / down / unknown counts |
| Database | db-performance | live sessions, connections of the maximum, cache hit ratio, blocking chains |
| ESB | esb | queue depth, dead letters, in flight, broker down |
| Alerts | alerts | unread notifications and the latest one |
| Events | realtime |
events per minute on the stream the TUI keeps open, with a ten-minute sparkline |
| Server | — | ping and UI API version (no request) |
| table | — | record counts of up to three favourite tables: :saver tables person,order (per profile) |
Styles (F9 › Settings › Screen saver style, :saver style <s>; auto cycles every three minutes):
Bounce (Query, the qrun.io mascot, and the name of the profile you are connected to in block letters
drift DVD-style and change colour at every wall, with the tiles in a panel riding beside them),
Starfield (stars fly out of the centre, Query drifts through, the tiles float), Flip clock (big
flip digits, the date and server, tiles beneath, Query peeking from a corner) and Big numbers (the most
interesting metric in block digits with Query beside it, or below it on a narrow terminal, the caption and
the other tiles under it as one centred stack, rotating every 8 s). In a tall narrow terminal Bounce stacks
the tile panel under Query. The block letters say the profile's
short name when it has 6 characters or fewer (else the profile name), with the server host in plain text
beneath; letters, digits, -, _ and . are drawn in block letters, anything else falls back to plain
text. Query blinks, wags and hops every ten seconds and faces the way he drifts. His art is Query's own drawing from https://www.qrun.io/query/ (one colour, indigo #818cf8) rasterised into braille at three sizes (42×22, 25×13 and 17×9 cells; the medium is used where there is room, from 100 columns) by scripts/gen-query-art.mjs; the art (braille, with a
line-art version for --ascii) is in src/saver/query-art.ts.
Burn-in guard (F9 › Settings › Screen saver burn-in guard, :saver burnin on|off, default on): nothing
stays put, so an OLED or CRT does not keep a ghost of the saver. The clock · server · profile line and the
"locked · any key to unlock" line move to a new place every 30 s (a random column, the top and bottom rows
swapping); Bounce's tile panel follows the logo, on the side with room (if Query and the panel do not fit
together, the panel shows for 20 s and then Query for 20 s, both drifting); the flip clock's cards and tiles
drift a cell every few seconds inside a small box; Big numbers moves its whole stack to another spot near the centre at each rotation;
Starfield's tiles keep floating. Each style is one compact group (Big numbers and Flip clock are at most about
90 columns wide however wide the terminal, the tiles in one or two adjacent columns), and the clock line and
the lock line sit just above and below it, swapping every 30 s. The clock stays visible throughout. off keeps the fixed layout (clock
and server on the top row, the hint on the last, tiles in the bottom-right corner).
Dimming (F9 › Settings › Screen saver dim, :saver dim <minutes|off>, default 10): once the saver has been
up that long, every colour fades toward the background. Both settings are saved in the saver section.
Animation runs at 4 frames a second at most and about one when nothing moves; only the saver repaints.
Large instances
Palettes, F9 and the Apps-pane filter search a kernel index built once per metadata document
(MetaDataIndex): nothing lists every table. App dashboards load widgets only as they come into view, four
requests at a time, cancelled when the screen is left. bun run packages/tui/scripts/fake-big-server.ts
serves a generated 1000-table, 500-process, 3000-widget instance on port 8099 to try it
(qqq ui --server http://localhost:8099).
Settings kept locally
Everything below is local to the machine and shared with the CLI; nothing is sent to the server.
~/.config/qqq/config.json(QQQ_CONFIG_DIR), owned by the kernel:profiles(server; optionalshort,description,authMode(auto,browser,device), the 1.0.0-rc.1note,lastUsedAtanduserrecorded by the UI, andui.themefor that profile only) anddefaultProfile. Fields are only ever added: a 1.0.0-rc.1 file loads unchanged. Sign-ins themselves are in the OS keychain (or the file keyring), one per profile.ui.theme: the theme for every profile without its own;ui.mouse:falsewhen the mouse is off;ui.splash:offwhenqqq uistarts without the splash.ui.dates(global, or in a profile'sui): date order (YMD,MDY,DMY),clock(24h,12h),weekStart,weekNumbers,pickerOnEnter, set with:dates. Without them the clock and the first day of the week follow the locale (LC_ALL,LC_TIME,LANG), else 24-hour and Monday; the order is ISO.ui.views: per screen.table:<name>columns (in order), widths, fit (content or fill), stripes, header rule, density, sort, pinned columns;record:<name>one or two columns and the label width;app:<name>widget order;widget:<name>dropdown choices of widgets that ask to keep them.ui.files: download and upload folders and whether a download asks or saves silently.ui.lock: the screen-lock PIN (a salted hash, never the PIN) and the idle minutes (0 = off).saver: the screen saver style, refresh seconds,burnin(default on) anddimMinutes(default 10, 0 off);profiles.<name>.saver.tables: favourite tables.ui.doctor: the answers to the terminal check;ui.recents: recently viewed, per server.
~/.local/state/qqq/(QQQ_STATE_DIR): the:and filter histories (history-commands,history-filters); lines that look like secrets are not kept.activity/<profile>.jsonl: the activity log the History screen andqqq historyread (one JSON event per line, rotated once to.1at 5 MB; secrets are written as***;QQQ_HISTORY=offstops it).~/.cache/qqq/(QQQ_CACHE_DIR): the metadata cache.
Themes: high-contrast (the default: bright text on black, strong selection), classic (the blue
function-key look), light (dark text on a light background). Choose with :theme <name>, F9 › Settings ›
Theme (for this profile or for all), or --theme; the option wins over the profile, the profile over the
global choice. --ascii replaces box drawing and symbols. Colours come only from the tokens in
src/theme.ts.
The CLI equivalent dialog copies its command with y (or Enter); F9 › Settings can copy it automatically, and Shift+drag selects it with the terminal.
F9 › Settings also holds the mouse on/off, the splash on start on/off, single-key shortcuts on/off (default: the server preference
keyboard.singleKeyShortcuts), the download and upload folders, the PIN and idle lock, and the terminal check.
Keys that your terminal may intercept
The TUI runs in the alternate screen and reads PgUp/PgDn itself, but a terminal or tmux can take a key
before any program sees it. qqq ui --check (and the quick check at startup) finds these and offers fixes.
-
tmux. A root binding such as
bind -n PageUp if-shell -F "#{pane_in_mode}" "send-keys PageUp" "copy-mode -eu"enters copy-mode on every PgUp, full-screen program or not. Let full-screen programs have the keys and keep scrollback in the shell:bind -n PageUp if-shell -F "#{alternate_on}" "send-keys PageUp" "copy-mode -eu" bind -n PageDown if-shell -F "#{alternate_on}" "send-keys PageDown" ""Keep
set -g alternate-screen on,set -g default-terminal "tmux-256color"and true color (set -as terminal-overrides ',*:Tc');set -g mouse onlets clicks and the wheel through to the TUI (see Mouse). -
iTerm2. If PgUp/PgDn scroll the window, map Page Up/Down in Profiles › Keys › Key Mappings to "Send Escape Sequence"
[5~and[6~; "Scroll wheel sends arrow keys in alternate screen mode" affects the wheel only. -
Terminal.app. F1 to F10 go to macOS unless "Use F1, F2, etc. keys as standard function keys" is on (System Settings › Keyboard), or hold fn.
-
Whatever the terminal does, Ctrl+F / Ctrl+B page and the F9 menu and
:line reach every action; Esc then a digit (or Ctrl+digit) stands for F1..F10. -
Shift+F-keys (the Shift bank): see Shift bank for what each terminal sends;
QQQ_FKEYSoverrides the legacy numbering.
Tests
cd packages/tui && bun test # or npm run test:tui at the root: the whole TUI against a fake server in memory
packages/tui/scripts/smoke.sh # tmux against a running server (profile "sample"); captures kept
# --server URL: an isolated config with two profiles, through the start-up picker
packages/tui/scripts/mouse-smoke.sh # the mouse in tmux: SGR 1006 clicks, drags and wheel written into the pane
packages/tui/scripts/smoke-widths.sh # fixed grid widths across pages (fake large-instance server)
packages/tui/scripts/smoke-sql.sh [server] # SQL Runner at 80x24 and 120x36 (default the enterprise server on 8078)
packages/tui/scripts/overflow-sweep.sh [server] # every screen and dialog (the splash, its picker, failure and add form too) at 80/100/120/200 x 24/50: no text past a border
packages/tui/scripts/perf-large-table.sh # PRF-001: 10,000 rows x 40 columns, timings against a 2 s budget
packages/tui/scripts/soak.sh [server] [minutes] # random keys for 10 min; fails on an exit or an error boundary
Layout of the code
src/store.ts holds all state and dispatches every key (lock, overlay, global F-keys, focused pane, fallbacks),
so handlers read live state, never a stale closure. Screens (src/screens/*) are route entries that keep their
own state (grid cursor, filter, selection, scroll), which is why Esc finds them as they were. Overlays
(src/overlays/*) are modal. Text fields are LineEditor objects driven by keys, not OpenTUI's focused
input, so a burst of keys lands in the field that had focus. Shared controls (src/controls/*) are the date control:
DateEditor (typed entry, a LineEditor), DatePickerOverlay (the calendar) and their wiring for forms,
bulk edit and the filter cell; a screen whose field wants F2 says so with claimsKey. src/app.tsx draws the frame with an error
boundary around every screen and overlay; src/main.tsx restores the terminal on any crash.