The record view's v dialog was a flat filterable list of 15 pre-baked
choices (label width +-4, five child row counts, the collaboration panel
listed three times). It is now a settings form like the grid's View
settings: Columns cycles auto/1/2, Label width (8-60) and Child list rows
(1-50) take typed digits or +/- with Backspace back to the default, one
Collaboration panel row, and Reset to defaults with a confirm. Persistence
keys are unchanged, so saved settings still load.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CLI qqq assistant ask/config, the TUI Assistant panel (Ctrl+G, :ask), the kernel Assistant module, the
base64 link filter fix and the enterprise server's ANTHROPIC_API_KEY handling are folded into the
structured entry with commit ids; the release notes get an Assistant section saying what the server needs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The assistant's links (and the web UI's) carry `filter` as base64 JSON,
which the TUI's link reader and the kernel's parseAppHref dropped, so a
filtered link opened the whole table. Both now decode it (JSON still
works), and a filter without criteria counts as an empty one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The enterprise profile now picks Claude when ANTHROPIC_API_KEY is set,
else a local Ollama. The key is read by name from the private assistant
env file (~/.local/state/qqq-enterprise/assistant.env: owned by you, not a
symlink, mode 0600; never sourced, never printed) and exported only into
the server's environment, not onto a command line. --status gains an
assistant line (and --assistant-status prints just that): the model that
answers, or the reason and what to configure.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A chat pane on the right (the whole body below 110 columns): question
line at the bottom, the conversation above with each answer's numbers and
links; Tab and Enter open a link as a record or grid through the existing
server-link checks. Questions carry the record or grid under the panel as
context like the Next UI (Ctrl+D drops it), history is kept per profile.
Ctrl+A was not taken because the grid uses it for its selection menu.
Without the extension the entry is disabled and Ctrl+G says what is
missing; when the assistant is off the panel shows the server's reason.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The assistant qbit was reachable only through its generated operation
subcommands, which cannot read a streamed turn. The kernel now has an
Assistant module (feature probe, ask with the page context the way the
Next UI panel sends it, an incremental SSE parser and an answer fold with
numbers and links), and the CLI builds two first-class verbs on it:
`assistant ask` streams the answer and prints its numbers and links with
the command that opens each; `assistant config` says whether it answers
and with which model, or the server's reason (NOT_CONFIGURED names what
to configure).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CodeQL flagged js/polynomial-redos (high) in the activity log's
redaction. Two regexes could backtrack polynomially on long arguments:
the header pattern (`Name:\s*(.+)`, slow on "A:" plus many spaces) and
the filter-criterion pattern (slow on long runs of name characters).
Redaction now uses linear scans:
- flags are parsed with indexOf at the first `=`
- headers and name=value pairs check the name before the first `:` or
`=`
- filter text goes through a one-pass tokenizer (spaces, names, symbol
operators, quoted strings, other runs), and the value after
`<secret name> <operator>` is replaced
The remaining regexes each have a single, unambiguous quantifier.
Behaviour is unchanged on every existing redaction test. A differential
fuzz against the old code (300k random inputs, not committed) shows
three kinds of difference, none of which exposes a secret:
- a value now stops at a `)` that closes a group
- a quoted value is hidden whole
- a header whose value is only whitespace is left as it is
The rest of the differences are malformed mixed quotes.
New tests run 18 adversarial 100k-character inputs, each well under
500 ms.
The Windows lane failed "events are appended one JSON line each". The
test asserted POSIX mode 0600, and Windows has no mode bits. That check
now runs only off Windows. The reader splits lines on /\r?\n/, and a
new test reads back a CRLF file. The offset test now checks the parsed
time and the offset that getTimezoneOffset gives for that instant,
whatever the zone's name.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The CHANGELOG bullets added by accretion since bedbf69 are merged into the structured entry (CLI history
and table stats, TUI history screen, one saver bullet instead of three fixes, Search coverage hint,
collaboration at the bottom, popups and widgets) with commit ids. Release notes, README and TRY-IT mention
history, stamped export names and the new keys.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The record view opened on three collaboration rows (tags, the counts line,
"Viewing now") between the title and the record's sections, and the cursor
landed on the tags row. The owner wants collaboration to sit with the
controls at the bottom and take no space at the top.
The record now starts right under its title, the cursor on its first field.
A dim strip directly above the key line carries tags, comment/file/task
counts, timeline, follow state and who is viewing; others viewing or editing
replace "(you)" (with their state, from presence's mode and field) and give
the strip an accent. Segments drop whole when the width is short, the
viewers and tags last. From 140 terminal columns a 36-column Collaboration
panel on the right shows tags, the latest two comments, open tasks with the
next due date, files, followers and each viewer's state, and the strip
shortens to the keys; a record view setting (collaboration panel
auto/on/off) controls it. The keys are unchanged; strip segments and panel
rows are click zones that press them. The summary now also fetches the
followers and keeps the latest comments and next due date for the panel.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Big numbers and Flip clock are one group at most about 90 columns wide
(tiles in one or two adjacent columns of at most 43 cells, the caption
capped), however wide the terminal; the guard drifts it a tenth of the
width at most. The clock line and the lock line now sit just above and
below the group (swapping every 30 s, a new offset each time) instead of
at the opposite screen edges. Tests cover six sizes including 200x50.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On a tall narrow terminal Big numbers squashed the number and Query into
a corner and left its only tile floating alone at the far left. Now the
metric name, number, Query (beside it, or below on a narrow screen),
caption, dots and the other tiles are one centred stack, and the guard
moves that stack as a unit between spots near the centre, always fully on
screen. A single tile sits centred under the number. Flip clock keeps its
tiles and Query with the clock as one group; Bounce stacks the tile panel
under Query when the terminal is too narrow for side by side. The drifting
header and lock line keep a one-cell left margin.
Layout tests cover five sizes (75x95, 200x30, 80x24, 120x40, 160x50) and
check every style stays within 40% of the group's centre.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The test asserted that the digits "2468" never appear in config.json,
but the stored salt and scrypt hash are random hex and can contain that
substring by chance, so the test failed now and then. It now checks the
stored values: the lock holds only pinSalt (32 hex) and pinHash (64 hex),
neither is the PIN, and no JSON value equals the PIN.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The owner asked for a "history" command that shows everything they have
done, like a bash history. Both heads now write to one activity log, an
append-only JSONL file per profile in the state directory
(~/.local/state/qqq/activity/<profile>.jsonl). Each event records the
time with the local offset, profile, server, client, kind, a one-line
summary and the command that does it again. The file is rotated once to
.1 at 5 MB. Secrets are written as ***: secret flags, name=value pairs,
filter criteria on secret or PASSWORD fields, Authorization headers and
bearer tokens. QQQ_HISTORY=off turns the log off.
- core: session/activity-log.ts (append, rotate, read, filter, redact,
CLI kinds, mutating-command check).
- cli: every invocation is recorded on exit with its exit code and
duration (shell completion is skipped). `qqq history` lists entries
with --limit, --since, --grep, --kind, --all-profiles, --absolute,
--commands and --format. `history rerun <n>` asks first for commands
that change data and records the command it ran. `history clear`
asks first.
- tui: one history.record(...) call at each site: `:` lines, opened
apps, tables, records and processes, queries with filters, record
create, edit and delete (no values), process outcomes, exports,
prints, server switches, sign-in and sign-out. The History screen
(`:history`, F9 > Work > History) filters as you type, Enter does an
entry again, y copies and c or F8 clears. Ctrl+R on the `:` line is
now a bash-style reverse search over the log.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
table stats refused tables without QUERY_STATS and pointed at --local. It now falls back
to the matching records itself (also when the columnStats process answers 403 or 404),
using the kernel computeLoadedColumnStats, says where the numbers come from, and exits 0.
--local forces it, --no-local keeps the old error, --max-records (default 10,000) caps it.
A reader that leaves early (qqq table query person | head -1) crashed the CLI with an
unhandled write EPIPE. stdout and stderr errors with EPIPE now exit 0 quietly, and a
stream pipeline rejected with EPIPE is not reported, so exports, process logs and
assistant ask behave the same.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The owner on the `:` popup: "this needs to be easier to read, text cut
off?" On a 160-column terminal the popup was about 50 columns wide, the
descriptions were right-aligned and cut with an ellipsis, and nothing
separated the name from the description.
The popup now takes the width its matches need, at least 60 and at most
120 columns, never past the screen's inner width. Rows are two
left-aligned columns, `name · description`; the name column fits the
longest name and gives way (down to 40%) only to descriptions that need the
room. A description that still does not fit wraps onto a second line under
the description column instead of being cut. The selected row is
highlighted across the whole popup. A dim header line carries the column
titles (command/argument/name and description or type/columns) and the
match count (on the border when the titles fill the line). The window
counts lines, so two-line rows keep the cursor visible, and the `:` list
stays above the command line with its height capped so the line and its
key help stay visible. The SQL editor popup gets the same layout (name ·
type / columns), below the cursor line or above it when there is more room
there.
Tests: the layout (width bounds, name column, wrapping, line-counting
window) and the `:` line end to end (header, separator, a wrapped
description).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A search with no matches said "no records match", which reads as "the
record does not exist" when the table is simply not indexed. The empty
state now says what quick search covers (N of M tables, up to five names
then "and N more", records, last reindex), that other tables cannot be
found there, where to add them (Quick Search Admin › Quick Search
Index), and that `o` searches the table Search came from by name (its
text fields, through the query API). The status line adds the indexed
table count ("index 57 records in 17 tables"). The kernel's
searchCoverageText builds the sentence.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The owner on Quick Search Admin's Search Diagnostics: "the header text
needs to have more layout or spacing". The widget's HTML went through
htmlToText into one block: sub-headings looked like body text, status
words were inline, label/value lines unaligned, paragraphs ran the full
pane with no blank lines, ISO timestamps had microseconds, and the Tools
paragraph was a run-on list.
Text and HTML widgets are now parsed into a small document
(src/text-doc.ts) and drawn with the hierarchy the web gives the same
HTML: headings (h1-h4, markdown `#`, or a short line before a block) in
the accent colour with a blank line before and their block indented;
a leading OK/WARN/FAIL/DEGRADED/DOWN as a fixed-width coloured badge so
lines align; `Label: value` items as an aligned two-column list, with
none/0 under a failure label in green; instants in the profile's date
format; body text at a ~100-cell measure with blank lines between
paragraphs; a sentence enumerating three or more capitalised names as
bullets; stat cards kept together; rows of button links as items Enter
follows (through the safe link parser). Lines carry coloured segments,
kept by the clipping and the list cursor; ASCII uses [OK] and *.
Row-shaped HTML (the alerts setup checklist) keeps its aligned columns.
Tests use the Search Diagnostics and SQL Workbench payloads captured
from the enterprise server, before and after.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Why: two exports of the same table overwrote nothing but piled up as person.csv, person-1.csv,
person-2.csv, which say nothing about when they were taken. A default name now carries the local
date and time (person_2026-11-01_102316.csv), so exports are unique and sort by age.
One kernel helper, exportFileName(base, ext, now), is used by every surface that chooses a name:
table export, widget --csv and report route downloads, field saves from a record, the OpenAPI
document, and in the TUI the grid export, SQL Runner results, saved reports, widget exports and the
screen-text print fallback. A name the user gives (-o, a typed name in the file dialog) is left as
given; the dialog's name field is pre-filled with the stamped name. A name that comes from the
server keeps it unless it is taken, when the stamp goes before the extension (then -1, -2).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Bounce tiles panel, the clock/server/profile row and the lock line sat
still on an OLED/CRT. Now (burnin on, the default): the panel rides beside
the drifting logo on the side with room (alternating 20 s panel / 20 s
Query when both do not fit), the header and lock line change place every
30 s, the flip clock and its tiles drift, Big numbers changes corners, and
everything dims after 10 minutes (:saver burnin on|off, :saver dim
<minutes|off>, F9 Settings). The block-letter logo says the connected
profile's name (short name when 6 characters or fewer) with the server
host beneath, and the block font covers letters, digits - _ . with a
plain-text fallback.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The client document's 17 printing 2.0.0 operations are the generated qqq printing subcommands (print
agent side). printTableList and printText are processes the kernel runs, reached as qqq print list and
qqq print text, not as qqq printing subcommands. Every operation count in the docs is 219, the length of
OPERATIONS in @qrun-io/qqq-core (generated from the merged document); no 202 remains outside the rc.1 notes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 1.0.0 entry had grown by accretion (Added/Changed/Fixed since rc.1 beside older sections). It is now
one entry with Added, Changed, Fixed, Security and Known limitations, grouped by area (CLI, terminal UI,
kernel and client, packaging and CI), deduplicated, with the commit of each post-rc.1 change. The release
notes describe what qqq is, the UI keys, the CLI highlights, compatibility, install, known limitations and
the enterprise test server, and the command sequence matches the real tags, orb 0.8.33 and the tap and
bucket repositories. The client README and runbook counts and checklist are brought up to date.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
F2 is Search, / is the find-anything palette, Ctrl+O switches servers and Ctrl+P prints; Ctrl+P no
longer steps to the previous record. The CLI README lists print, printer and profile. The readiness
gate reported qqq print person 1 2 as a missing verb although record is print's default command.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The mouse footer of the message dialog ("any key or a click here closes ·
...") was cut mid-token in narrow dialogs; it now goes through hintLine. The
sweep's / scenario follows the new Search dialog.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
At most three lines of keys, broken between keys and dropping the least
important first, like the other dialogs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The F9 menu, the Servers dialog and the SQL Runner dialogs had their own key
line helpers measuring string length; they now call util/text fitKeyLine, and
the SQL Runner's segment fitting uses fitSegments (terminal cells). The overflow
sweep covers F9 levels 1 and 2, the Servers dialog and its add form, and, where
the sql-workbench extension is installed, the SQL Runner (SELECT, a table,
an error, a plan, the schema and the history).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The developer view now sizes itself at render time (f236c7b), so the LinesScreen
wrap-width hook added for the old screen is removed again. The overflow sweep
covers the record and table developer views, their sections, the JSON section
and a value opened from it. CHANGELOG follows.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The record view's key line now goes through hintLine with drop priorities
(A and D go first, Esc back is kept), so it never cuts a key at 80 columns.
V on a field row of the record view shows the whole value of its field(s) in
a dialog, as V does on the grid. The help's key table keeps two columns when
all but a couple of descriptions fit. The sweep adds a 30+ column grid
(db_perf_snapshot on the enterprise server, else fieldLab), the F9 menu with a
search, and waits longer for a large instance to start; the checker accepts a
box cut by the bottom of its scrolling pane even when a dialog covers that
pane's corner.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The owner saw the View settings dialog run text and its highlight bar past the
right border, and the key help cut in the middle of a token. Make clipping the
default at the container level instead of trusting each screen:
- util/text: trunc/pad/padLeft/center measure terminal cells (Bun.stringWidth)
so wide characters cannot push a border; fitSegments for styled lines;
fitKeyLine/hintLine lay key help out between tokens (" · "), dropping the
least important first (explicit `drop` priority; Esc/Enter/Ctrl+S kept).
- overlays/frame: Frame is held inside the screen and gives Line/KeyLines its
inner width through context; Line cuts with an ellipsis instead of slicing.
- View settings: the value cell holds only the value; the selected row is
explained on a help line ("columns that stay left while scrolling · +/-
changes"); keys in at most three lines; +/- really change Pinned columns.
- Sweep fixes: footers drop whole keys, help key table goes to one column
when two would cut it, developer panels and process errors wrap to the
pane, home header lines end in an ellipsis, saved column widths are held to
the grid, bulk edit intro fits 80 columns, wrap() keeps long words.
- V on the grid shows a cell's whole value.
- scripts/overflow-sweep.sh drives every screen and dialog at 80/100/120/200
x 24/50 against a running server; scripts/overflow-check.ts fails on a line
wider than the terminal or a broken box border. smoke.sh runs the check on
its captures.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Windows timers fire in ~15 ms steps, so the fixed tick(30) in the joinPresence
stream test ended before the three stub events (tick(2) each) were applied, and
the failed assertion skipped handle.stop(). The live handle's heartbeat interval
and endless stub stream then kept the test process alive, which CI saw as ten
silent minutes. Tests now poll for the state they assert and stop every handle
in finally. run-tests.mjs gives each test a 2 minute deadline and kills a run
that outlives 8 minutes with a message.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two more CodeQL js/polynomial-redos alerts on the release pull request: the
search screen's status-line clock (/^.* (\d\d:\d\d).*$/) and the date parser's
date/time split (/^(\S+?)(?:[T ](.+))?$/i). Both backtracked quadratically on
long repeated input. lastClockTime and splitDateAndTime in core's text helpers
return the same results by plain scanning; each has a 100k-character test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The enterprise script generated its MinIO credentials in the shared temp dir and sourced them, so
another local user could pre-create or symlink the file to run code or read the credentials. State
now lives in a 0700 per-user directory; existing files are used only if owned by us, not symlinks
(credentials also 0600); the credentials and pid files are parsed and validated, never sourced.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The enterprise profile installs qbit-object-storage when S3_ENDPOINT is
set, so collaboration attachments work. enterprise-server.sh now starts
qqq-enterprise-minio on 127.0.0.1:9310 alongside Redis and OpenSearch,
creates the attachments bucket, and passes S3_* to the server. The
credentials are generated once into $TMPDIR/qqq-enterprise-minio.env (0600;
QQQ_ENTERPRISE_MINIO_ENV overrides) and never written to the repository.
container_up takes a container command after "--".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scripts/enterprise-server.sh builds and runs qqq-all's enterprise profile
(branch feat/enterprise-terminal-test in .worktrees/qqq-all-enterprise)
on port 8078 against the UI API 1.9.0 fix-branch build: MOCK auth, every
locally runnable qbit, the qqq sample app, Redis and OpenSearch in Docker
for redis and quick search. --status and --stop only touch the pid it
started and the two containers it names; it never touches the 8077 sample
server.
scripts/enterprise-smoke.sh runs the CLI through each qbit surface (ESB,
alerts, assistant, collaboration, data analysis, DB performance, QA
capture, quick search, release notes, secured data, SQL workbench, jobs,
audits, saved views and reports, exports, webhooks, processes) and records
exit codes, so client and server gaps show as non-zero lines.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A record view prints its record sheet, a grid a table list with its
columns, widths, filter and sort (all rows, this page, or the selection),
and any other screen its text as a monospace PDF, then the printer picker
and the job progress line. Also p on records and grids, F8 where a screen
has no Delete, :print [printer], a and F9, and Print Screen when the
terminal passes it on. Without printing Ctrl+P saves instead: the grid's
export, else the screen text. Ctrl+P no longer steps to the previous
record; PgUp and [ still do.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
print list prints a table list of the whole result of the criteria (up to
10,000 rows) or one page, in the column order given, with paper and
orientation options; print text prints a file or stdin as a monospace
document. Both pick and remember the printer as print does and follow the
job.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
printTableList sends a grid's table, columns, widths, filter and sort with
the scope (all rows, this page, or selected rows) to printingPrintTableList
and passes the server's row-limit note on; printText prints text such as a
screen capture. listScopeChoices orders a grid's print choices: all rows
first when the grid is filtered or small, this page first otherwise, the
10,000-row limit named, and the selection as a list or as record sheets.
The client document's printing entry moves to 2.1.0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Uploading to a server without the Object Storage QBit showed the raw
"IllegalStateException (The Object Storage qbit is not installed)". A
server failure (5xx) naming a missing QBit or extension now reads as one
line, for attachments "This server cannot store attachments: the Object
Storage QBit is not installed." (the kernel's missingQBitMessage, used by
the TUI's errors and the CLI's error output, so `collab
post-records-attachments` says it too). Once known for a server, the
UI's "+ Upload a file" row and the record's upload action are dimmed
with that reason instead of starting another upload.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The owner: quick search is the main way to find a record and get into
the data, so it gets a function key and a screen of its own. F2 (it was
"Go to", which `/` covers) opens Search everywhere: a search line with
"search records across N tables (quick search)", results as you type
grouped by table with label and count, each hit's label, key and
highlight snippet with the field. ←→/Tab cycle the tables, Enter opens
the record and Esc returns to the hit, `o` opens the table's grid
filtered to the hits, Space marks hits for `a` (grid, export, print,
bulk, processes), `f` narrows to one table, `h` recent searches kept per
profile, `?` the syntax quick search really has. The status line shows
matches, tables, time, index size and the last reindex.
Home starts with the search line: typing there starts a search. Without
the extension F2 still opens Search and finds tables, apps and processes
by name and the records of the table it came from, saying "quick search
not installed: searching names only". `:search <text>` opens Search;
`:find <text>` and `/` stay the find-anything palette.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The kernel gains record search (query/record-search): POST /search
grouped by table with counts, each hit's label, key and highlighted
snippet with the field, what the quick search query language supports
(AND of words matched from their start; no phrases, field:value,
wildcards or OR), notes for text that uses those, and the quick search
status line. The CLI's `qqq search` uses it: a `match` column, `--table`
repeated or comma-separated, `--limit` per table, a footer with the
tables and the total, `field:value` searching the value with a note, the
syntax in --help; `--json` prints the response.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The S dialog was a dead end on tables without QUERY_STATS: it only said so. It now
computes count, nulls, distinct, min and max from the rows the grid has loaded, plus
sum, mean and median for numbers, shortest, longest and the top 5 values for text, and
earliest, latest and the top 5 days for dates, and says where the numbers come from.
A server that offers the process but answers 403 or 404 gets the same fallback. 'a'
pages through the existing query API (current filter and sort, up to 10,000 rows) with
a progress line; Esc stops and keeps the rows so far. The computation is a pure kernel
function (loaded-stats.ts) with unit tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The owner asked why the mouse did not work everywhere. Mouse tracking is now
on by default (button-event tracking with SGR 1006, no motion, so hovering
does nothing), switched by F9 Settings "Mouse: on/off" and :mouse (saved in
ui.mouse) or --no-mouse / --mouse for one run.
One transparent box over the terminal takes every event, so OpenTUI's own
text selection and focus never start; a registry of zones (rectangles
measured from the drawn elements, a layer per dialog) says what is under the
pointer. Clicks select, double-clicks activate, right clicks open the
actions at that row, the wheel scrolls 3 lines (Shift sideways in the grid),
header drags resize or move grid columns (saved as view settings), the
F-key bar presses keys and the footer opens the Servers dialog. Clicks
beside a dialog do nothing. Help and copyable-text dialogs say Shift+drag
selects text. The two-level F9 menu, the Servers dialog and the SQL Runner
panes are wired too.
The layer lives in src/mouse/*; screens add a few Zone wrappers.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner feedback on SELECT * FROM QQQTABLE: the results need to show better,
the export should be complete and discoverable, and Tab should list what
fits even before anything is typed.
Results: columns stay in the server's column list order (for QQQTABLE that
is the table's own H2 order, LABEL, CREATEDATE, MODIFYDATE, NAME, ID, as the
execute response sends it). Cells use type-aware formatting: dates and date-times in the profile's
display format (the kernel's formatDateValue and the TUI's date settings:
day order, 12/24 h clock), seconds only when the value has them, never the
fraction, zoned instants in the local zone; numbers
right-aligned with thousands separators except key-like columns, booleans,
NULL as a dim dash. The grid gets a row-number gutter, a header rule, zebra
per the sql:results view setting, a selection bar that ends at the last
column, horizontal scroll with Shift+Left/Right and edge markers. The status
line sits directly under the results ("1 row · 7 ms · h2 · read-only ·
limit 10,000 · sorted by —") with the key line under it, and F8 Export
joins the F-key bar.
Export and copy: F8 and x open the main grid's export dialog with csv, tsv,
xlsx, json (every row: the server writes csv/xlsx/json from its cached
result, tsv is read page by page) and copy to the clipboard for small
results, through the file navigator and the download-folder preference.
y copies the cell, Y the row as TSV, Ctrl+Y the whole result. Export and
copy show in the a actions menu, F9 Context and F1.
Completion: Tab in the SQL editor opens a popup also with nothing typed
(after FROM/JOIN every table with its column count, after SELECT/WHERE/ON
or a dot the statement's columns with types, at the start the statement
keywords and recent statements), filtered as you type. The : command line
uses the same popup: every command with a one-line description on an empty
line (TUI commands plus the CLI's own descriptions), then a command's
arguments. The popup clips to its width and follows the ASCII setting.
Execution UUIDs and cell keys now always come from crypto.randomUUID()
(the old Math.random fallback was flagged by CodeQL, and the id goes to the
server).
Tests: column order, cell formatting, delimited output, export choices,
reading every row, the popup model, empty-prefix completion, and the screen
and : line end to end. smoke-sql.sh runs the owner's query, both popups and
a real csv export whose row count must match the status line.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The saver was a drifting clock. It now shows what is happening on the
server: people online (collaboration presence), jobs, health, database
sessions, ESB depth and dead letters, unread alerts, realtime events per
minute, ping, and the profile's favourite table counts. Each tile hides
when its extension is absent (or answers 403/404) so the plain sample
server works too; failures keep the last value dimmed as stale and back
off. Four styles (Bounce, Starfield, Flip clock, Big numbers, or auto)
animate Query, traced from the line drawing on qrun.io/query, at no more
than 4 fps, and only the saver repaints. The lock's key handling is
reused unchanged: with a PIN the prompt opens over the animating saver.
`:saver [style]` previews; F9 > Settings holds style and refresh.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dates and date-times were typed as bare ISO text with no help; the owner asked
that they can be entered by hand or picked. One control now serves every place a
DATE or DATE_TIME is edited: create/edit forms, process forms, bulk edit and the
filter dialog's value cell (its row model unchanged).
Kernel (forms/dates.ts): parseDateInput reads ISO, the display format, partial
dates, t/today, n/now and the filter grammar's now(...)/startOf(...), refusing
with the exact reason; slash dates only when the profile's date order says which
part is the month. formatDateValue/formatHint, monthGrid and week numbers, and
ui.dates settings (order, clock, week start, week numbers, picker on Enter).
Values go out as the kernel already sends them.
TUI (controls/): DateEditor (a LineEditor showing the display format, Backspace
clears a whole value when nullable), DatePickerOverlay anchored to the field
(F2/Ctrl+Space; time row with zone, UTC sent and server zone), the bulk prompt
(now sending DATE_TIME as the UTC instant like forms), the filter wiring, and
:dates. Screens claim F2 through Screen.claimsKey so it is not the palette there.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The owner asked whether anyone was getting the SQL Runner working in the
terminal. The Next UI's Runner (a SQL_WORKBENCH widget on its own app page)
had no TUI screen: its app opened an empty app home.
Add a classic database-TUI screen over the sql-workbench UI API extension:
- editor on top: multi-line buffer, syntax tint (server keywords included),
F5 / Ctrl+Enter (Ctrl+J) runs the statement under the cursor, Tab completes
tables and columns (alias.columns) from the schema endpoint, Ctrl+Up/Down
steps through the on-disk run history; :name parameters are asked for,
bound as values and remembered
- result grid below: server-paged and server-sorted over the cached result,
column widths from the column kinds, NULL dimmed, V whole value, x export
(csv/xlsx/json through the file flow), status line with rows, elapsed,
backend, read-only and the row limit (cursor tokens dropped first)
- errors inline under the editor with the SQL position marked (H2 [*],
PostgreSQL Position, line/column forms)
- F6 EXPLAIN as an indented, foldable plan tree with costs and a relative
cost bar (raw text with r); quick text bar charts (c)
- F3 schema browser (backends, tables, columns; filter; Enter inserts)
- F2 notebooks (mine/shared, open, save as a new version, save as,
duplicate, delete, publish when allowed), F4 cells (SQL and markdown,
markdown rendered as text, move, title, backend, run all), F7 audited
history with open and re-run
- reachable from the Runner app in the tree, `/` (the SQL Workbench widget
hit opens the Runner instead of `widget get`), F9 > Work and :sql
(:sql <statement>, :sql open <id>); without the extension the Runner apps
and their SQL_WORKBENCH widgets leave the metadata and :sql says the
extension is missing
Registration is kept to a few lines: store.setMeta hides the Runner apps
(extensions now load before the metadata), a screen may claim a global key
(F2 here; its keys reach F1 through the existing helpKeys), openApp routes
Runner apps, and commands gain :sql, the Work entry and the widget route. Write mode is not offered (the Next UI keeps
the gated write flow).
Tests: editor model, completion, statements, parameters, error positions,
history, notebooks, absent-extension metadata, result paging and sort,
column widths, plan tree, bars, and the screen end to end against a fake
server (80x24 clipping checked). scripts/smoke-sql.sh drives it in tmux at
80x24 and 120x36 against a server with the extension.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Switching servers took the profile chooser and gave no sign of which servers
were up. The dialog lists every profile with short name, server, background
probe status, last use, signed-in user and description; Enter switches
(reusing a valid stored sign-in), n/e/d/* add, edit, remove and set the
default, typing filters. Home and the footer show the profile's short name,
the switch fetches fresh metadata, and the footer clock and status are reset
for the new server instead of freezing on the old one.
With the flat app and recent entries gone from F9 (places are found with /),
Go to lists the apps and the recent records as groups (five and more…), so
those lists stay one level down in the menu; Ctrl+O joins the key reference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The flat three-column list of every entry did not fit and was hard to scan.
Level 1 lists the areas with a count and a summary; Enter or the right arrow
opens one, a letter jumps to it, left or Esc goes back, Tab steps areas. Long
groups (Recent, Apps, Favorites, or any entry's group) show five and more….
Two or more typed characters still search every entry, now listed by area.
Only the structure and rendering changed: the entry catalog in commands.ts
is untouched apart from the optional group field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Profiles gain short (2-6 chars, for compact places), description, authMode
(auto, browser, device for OAuth2 servers), and lastUsedAt/user recorded by
the TUI. Every field is additive: a 1.0.0-rc.1 config.json loads unchanged and
its note shows where a description is missing. probeServer asks /version and
the authentication metadata with a short timeout so the CLI and the TUI's
Servers dialog report reachability the same way.
qqq profile list|add|edit|use|remove (also under qqq config profile) matches
the dialog field for field: --short, --description, --auth-mode, --check,
edit --server/--rename, list --probe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/print-agent.sh runs qqq-print-agent's virtual file printer against
a local server with the printing qbit (the enterprise server on 8078 by
default): it enrolls and approves itself the first time and writes each
job to a directory. README and CHANGELOG (1.0.0, Added) describe qqq print,
qqq printer and the TUI's Print action.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GET /qbit/printing/session must answer 200 with principalId and tenantId
(or 403 with the error schema when the user lacks printing access), and the
printing printer, device and job tables must query and validate.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
p (Print...) on a record, or on a grid's Space selection, opens a printer
picker: printable printers first, the others listed with the reason, the
last used one preselected and remembered per profile. The jobs are followed
in the title bar and status line (print 1/3 RECEIVED) with a flash when they
end, and :printjobs (F9 > Work) lists recent jobs. Without the printing
extension no Print action is offered and :printjobs says what is missing.
The qbit's own Print process is not listed again under Run.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
qqq print <table> <id...> prints records as record sheets and follows the
jobs until they have printed (--printer, --copies, --no-wait, --timeout;
exit 1 when a job failed, 4 when still running at the timeout). qqq print
job list|status [--watch]|cancel follows and cancels jobs; qqq printer
list|use|show|forget|devices shows printers and print agents and manages
the printer remembered for the profile. Without the printing extension the
commands exit 5 with the installed list.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Printing wraps what a person does with the printing qbit: printers (with
approval, exposure and formats), print devices and their health, jobs by id
or state, Print on a table's records through its printingPrintRecords_{table}
process (returning the queued job ids), cancel, and watch until every job
is final. resolvePrinter and whyNotPrintable serve both heads, and the
printer chosen last is remembered per profile in config.json. Every call
needs the extension and throws ExtensionNotInstalledError before any
request otherwise.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Appends qbit-printing's committed extension document (printing 2.0.0,
17 operations under /qbit/printing) to the client document with
merge-spec.mjs, keeping the other documents untouched, and regenerates the
schema and operation index (219 operations). The registry knows printing
with minimum 2.0.0: the realtime channel became the agent's prerequisite
there, so a 1.x server counts as not installed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Steps 13b-13d open person 12 and its ESB events, which only the sample
server has; with --server they are skipped (SAMPLE=1 keeps them), so the
smoke test passes against the enterprise server too.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review of the widget links: parseLink turned any `:…` link into a command
that Enter ran, and links come from server content (widget payloads,
HTML, table rows with __href), so a buggy or malicious server could make
Enter run `:process run …` or `:table delete …`.
- Server links are read against the metadata: `/app/{table}[/{id}]` and
`/app/{process}` through the developer view's linkTarget, the web's
app paths only when every app on the way is an app; record keys must be
plain segments, filters must have a QueryFilter's shape. `:` text,
unknown names and other sites are inert notices that show the address
(no browser).
- Only TUI-built links are trusted: ESB destination rows carry a
symbol-keyed link that JSON cannot contain. Commands they run are shown
in the status line first, server names are quoted as single arguments,
and mutating commands confirm.
- Extension output is now laid out by the kernel's formatDocument (the
CLI's layout since 5e34433) instead of a second renderer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CHANGELOG 1.0.0 (changed, added and fixed since rc.1), the TUI README keys
and sections, and TRY-IT: `?` is the keyboard reference, `/` the one
search (with :search), the record's collaboration keys, J for raw JSON.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Against another server the smoke test launched the TUI without a
profile for it and landed on the profile picker. With --server it writes
an isolated config (config, cache, state, file keyring) with one profile
for the server, so a MOCK or anonymous server signs in straight away and
the user's own config is untouched. The positional profile still works.
The help step checks the new keyboard reference.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The record view offered only tags from the collaboration extension. It
now shows a line with the record's comments, attachments and open tasks
and the follow state (keys C P K T W), and below it who is viewing the
record now: everyone the server reports, this user marked "(you)" (the
MOCK and same-user case merges into one person, so others alone could
never show). `a` and F9 list comments (list, add, reply), follow and stop
following, attachments (list, download through the file navigator,
upload, delete), the timeline, tasks (list, add, done) and the share
preview (find people, or type user ids when the directory is empty; see
who could read the record, then send). Without the extension the line
is not drawn and F9 says "needs the collaboration extension".
Comments, following and people go through the kernel's Collaboration;
attachments, timeline, tasks and shares are the extension's operations.
An attachment's temporary URL gets credentials only when it is on the
server's own origin.
Also: a choice list sizes itself for disabled reasons too (they were cut).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Closing the tmux pane or window before quitting printed "setRawMode
failed with errno: 2" and a native session OutputFailed error, each with
a stack trace. The renderer's restore steps cannot succeed then and the
messages say nothing to the person who closed the window; they are
dropped from console.error/warn and from the crash handler. Other errors
still print.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cards printed "[notifications_active]". Common Material icon names map to
single-width glyphs (bell, chart, database, people, settings, search,
star, folder, terminal, shield, hub, clock, ...), with ASCII stand-ins
under the ASCII-glyphs setting; unknown names fall back to the app mark.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Help and go-to overlapped: `?` listed actions and searched help, `/`
listed recents and commands, F9 listed every app and metadata match.
Each thing now has one home.
- `/` (F2, `.`, Ctrl+K, `:search <text>`) searches apps, tables,
processes, widgets, reports, records, commands and help, grouped under
headers with the best match first. Records come from quick search (or
the server's search), debounced; the group reads "quick search: N
matches" with the table and record label per hit, and says once,
dimmed, "records: install the quick search extension" without it.
Hits open by `recordId` (they opened with an empty key) and the label
falls back to the highlighted text when it is only the key.
- `?` is the keyboard reference of the screen: keys grouped by what they
do (the screen's own shortcuts from its actions, keys only), the
screen's help text, and "/ search anything · F9 menu · : command line".
- F9 keeps its sections and drops the per-app, recent and index entries.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The enterprise sweep found widgets the terminal could not draw: the ESB
app's overview printed "(ESB_OVERVIEW: no terminal rendering yet)", the
jobs statistics printed their web addresses, the alerts setup checklist
ran label, status and detail together, and extension output dumped
nested arrays as one-line JSON.
- ESB_OVERVIEW is drawn from the esb extension as `:esb overview` does;
Enter on a destination offers the ESB of its table and processes, its
messages and dead letters.
- Widget lines carry links (a statistic's url, a table row, an HTML
row's first link); Enter reads the table, filter, record, process or
app out of the web path and opens the TUI's own screen.
- Row-shaped HTML becomes aligned columns, the last wrapping under
itself; the kernel decodes the common named and numeric entities.
- Extension operations on `:` and payloads without a block draw as
key/value rows and clipped sub-tables; J shows the raw JSON.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The CLI equivalent dialog could only be read. y, c or Enter now copies the command
(Esc closes without copying), the filter dialog gets Ctrl+Y for its command line, and
F9 > Settings has "Copy CLI commands to the clipboard automatically" (ui.copyCliCommands,
default off). The footer points at Shift+drag for terminal selection; mouse tracking is
left to the TUI-wide mouse setting.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CHANGELOG 1.0.0 gains the -o/--order convention with its migration note
and the extension table output under "Changed since 1.0.0-rc.1", and the
ESB trigger, login profile, health fallback and conformance fixes under a
new "Fixed since 1.0.0-rc.1". README, the cli and conformance package
READMEs, TRY-IT, the 1.0.0 release notes and the parity matrix (and its
generator) use --order for the sort and -o/--out for files.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
esb messages on a topic without --trigger only passed on the server's
"name one of the triggers", and trigger names look like
syncOrder.orderEvents, so the process name syncOrder answered 404. On a
400 or 404 the command now reads the ESB overview: a process name (or
label) with one trigger on the topic reads that trigger, several are
listed, and otherwise the error lists the valid trigger names
(resolveEsbTrigger in the kernel).
health get-summary said "not installed" on servers that serve /health
without listing the health extension in GET /version (qqq-all). The
kernel's probeHealth reads /health, shapes it like the extension's summary
and marks it unadvertised; the CLI shows it with a note when the
extension is not listed and the endpoint answers.
Tests cover the sub-table golden output, --columns, the -o convention,
ESB trigger resolution, the login warning and the health fallback against
a fake enterprise server.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The generated QBit commands (alerts get-inbox, sql-workbench
get-capabilities, collab get-..., api) printed lists as one line of JSON
in a key/value table (`items [{"id":5,...`). The kernel's render model
gains a document layout (formatDocument): scalars as aligned key/value
rows with nested objects as dotted keys, each list of objects as a
sub-table under its key with columns from the union of the row keys in
first-seen order, nested objects flattened one level, lists in a cell
joined, fitted to the terminal width (cells cut with an ellipsis, columns
that do not fit named under the table). render() uses it for nested
results and for any result given a layout; json, yaml, csv and text do not
change, and a --query reshape drops the column choice.
The inbox, notebooks, SQL history, comments, tasks, favorites and recents
get default columns (DEFAULT_LIST_COLUMNS); --columns (or the global
--fields) chooses, on the generated commands and on the first-class collab
comments list, favorites list, recents, tags list and people.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
table export and table query used -o for --order while the extension and
download commands used it for the output file, so `table export customer
-o file.csv` sorted by a field named after the path (and the server
answered 403). One convention now: -o/--out is always the output file, the
sort is --order (no short form; -s is --set). table export takes
-o/--out. On the commands that write no file (table query, saved-view
save, saved-report preview and set) -o still sorts for one release, hidden
from help, with a deprecation note. A sort field that looks like a file
path is warned about. Completion offers fields after --order only and
skips hidden options; the flat query alias forwards short-only options.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With QQQ_PROFILE set, `qqq login --server URL` silently moved that profile
to another server. resolveProfile now reports movedFrom when an existing
profile is given a different server; login warns, needs --yes (or a yes at
the question on a terminal) and says --profile <new-name> adds a profile
instead. saveLogin only moves a profile when login asked for it, so a token
refresh under a one-off --server no longer moves it either.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GET /jobs and the realtime check built their own credentials while the
other checks used the token as a bearer only, so a token could report "no
jobs permission" where a session worked. One Authentication now serves
every authenticated check: a JWT as a bearer token, a session UUID also as
the sessionUUID cookie, and on MOCK and FULLY_ANONYMOUS servers without a
token a server session from manageSession (what qqq login does there). The
metaData check names the authentication used, and a jobs 403 says it was
the same one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The record and table developer views were one wall of text: panel blocks
flattened to prose, tables with ad-hoc headers, web paths as links, and the
ESB sample event broken into fragments that looked duplicated. That came
from the prose word-wrap: the minified event was split at the spaces inside
its strings ("Clone of: Blair", in both record and oldRecord) and every
longer piece was cut at the width.
Both views are now a section bar (left/right or 1-9) over a scrollable pane
with a scroll bar. Record: Security, Events, Scripts (revisions, code, logs,
test runs, store from $EDITOR), JSON (endpoints, display and raw values),
Metadata, Audits, APIs, then any further panel. Table: Events (ESB counters),
JSON, Metadata, APIs. Grids use the main grid's column layout with a dash
for no value and a note on why a column is empty; JSON is pretty-printed,
tinted and cut per line, with v for the full value viewer and y to copy.
Web links map to TUI screens or are shown as browser only. F3 goes to the
record or the grid; the help overlay lists the developer keys.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Core now prints quoted filter values with \" and \\ escapes and its
tokenizers skip an escaped quote. mixedJoinAt, which points at the first
mixed AND/OR in the Advanced text, toggled on every quote, so a value such
as "x \" AND y" was read as ending early and its AND flagged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
post_logout_redirect_uri is rebuilt from a constant loopback host, a numeric
port and its path, and any other value is answered with 400 (it showed the
signed-out page before), matching the authorization redirect_uri rule.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
editText ran $EDITOR through a shell with a file name built from the table,
primary key and field, so a primary key such as $(...) executed. The path is
now the shell's $1 on POSIX (as git runs its editor) or a quoted variable on
Windows, and the name keeps only letters, digits and ._-.
Also: --api-path trimming and bulk help text use the linear core helpers.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Server metadata and user input reached regular expressions that backtrack
quadratically (slash and = trimming, ${name} and ${input.x} references,
filter error tails, tmux list-keys lines). They are replaced by linear scans
in render/text.ts, with the same matches for well-formed input.
htmlToText and the upload-template names decoded entities in sequence, so
&lt; became <; they now strip tags in one pass and decode once. Filter
values escape backslashes as well as quotes, and the tokenizers count
preceding backslashes, so a value ending in a backslash keeps its closing
quote.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The guided dialog mixed three models: quick rows that reused one
editor call (a true/false quick field became the unary TRUE and its row
edit re-applied TRUE, so it could not be changed there), condition lines
edited through a menu, and letter shortcuts next to cells that took
typing. People could not tell which row did what or how to change a
value.
Now every condition is one row of three cells, edited the way its type
asks: text and numbers in place, choices from a searchable list that
toggles values, Space cycles true/false (t/f set it), dates take presets
or typed values, and the operator cell lists the operators that fit the
field. A row without a value shows (any) and is not applied; Backspace
clears any cell. Quick fields are the same rows. AND/OR and groups are
drawn as structure, the help and key lines follow the focus, and the
footer shows a live, debounced match count. Commands moved to Ctrl and
F keys so letters always type; Esc asks before discarding changes.
Advanced gains a cheat sheet, a caret that follows the scrolled text
(and points at the joiner when AND and OR are mixed), and says why it
cannot switch back to Guided.
Design: packages/tui/docs/filter-dialog.md. The smoke script switches
modes with F2 (Tab now moves between cells) and the soak's reset accepts
discarding a half-edited filter.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The cli-windows-x64 lane failed in every package with "Could not find
...\dist\test\*.test.js": npm runs scripts with cmd.exe there, which
does not expand globs, and Node 20 does not glob its own arguments.
Plain "node --test" is no fix on Node 20 either, because its default
patterns run every file under a test directory, helpers such as
harness.js and stub.js included.
scripts/run-tests.mjs lists the *.test.js files directly in dist/test and
spawns node --test with them, so all four packages run the same files on
every platform. Counts on macOS are unchanged: 18, 167, 3 and 100.
Two cheap Windows fixes in the CLI test harness: the sandbox sets
USERPROFILE as well as HOME (os.homedir() reads USERPROFILE on Windows,
so defaults would have escaped into the real profile) and QQQ_STATE_DIR,
and the golden-file normaliser turns backslashes in sandbox paths into /
so export-download-dir.txt matches there too.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The heavy-dashboard test proved concurrent loading by requiring the
whole command, CLI start-up included, to finish under 3000 ms against a
stub that answered each widget after 150 ms. A serial load would take
3.6 s, so the margin was 600 ms of start-up on whatever machine runs it:
the same kind of wall-clock guard that failed the release pipeline in the
core index test.
The stub now counts widget requests in flight and holds each answer until
every widget request has arrived (or 300 ms after its own arrival). The
test asserts the peak is above 1, which a serial loader cannot reach;
loadDashboard has no concurrency cap, so local runs also assert all 24
overlap and keep a generous 6 s bound, both skipped when CI is set.
Checked by making loadDashboard serial in the built kernel: the test then
fails with a peak of 1, CI set or not. Three npm test runs under
nice -n 19 with competing busy loops, and one with CI=1, all passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paging record to record on a table with child lists (the sample's sales
order) moved every section below the lists, and the same association was
drawn twice.
Cause of the movement: child lists grew and shrank with the record's row
count, empty grandchild lists vanished, and field and child-list column
widths were measured from the values of the first record shown. Now the
section plan comes from the metadata only; each child list is a box of
fixed height (heading, column header, N data rows, a count line) that pads
short lists, keeps its box when empty, and scrolls long ones inside the
box ("rows 1-5 of 12"; PgDn/PgUp a box, Down/Up or j/k a row at the edges,
] and [ still step records, Enter opens the child). Column positions come
from field metadata. Widget sections keep the tallest height they reached,
the collaboration tags line is always there, and a child's own child lists
(whose number varies) come last. N is the per-table view setting
childRows (v), defaulting to ui.childRows of the profile, then the global
ui.childRows, then 5 (kernel readChildRows).
Cause of the duplication: two code paths drew the association. The
section loop rendered the childRecordList widget section from its widget
payload ("Lines" with the "1 Sales Order Line" caption), and the
associationGroups loop after the sections rendered the record's
associatedRecords for the same association ("Lines (1)"). The Next UI
treats the widget section as the place of the association: a section
whose childRecordList binds an association renders that association there
under the section label, and the association is dropped from the related
list. The kernel's widgetAssociation binds as Next does
(manageAssociationName, rowBuilder associationName) plus the join name the
sample's widgets carry; the TUI draws the bound list from associatedRecords
with the widget's column set (kernel childWidgetColumns, Next's
childColumns without the parent key) and no longer loads its widget
payload. Widgets that list no association of the table stay as they are.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The CLI ships for macOS and Windows and has Keychain and DPAPI keyring
paths, but CI only ran Linux. qqq-orb 0.8.33 adds opt-in platform lanes to
the cli profile, and the setup config's 0.8 track picks the release up by
itself, so the manifest only needs platforms: [macos-arm64, windows-x64].
Rendered with the orb's generate_pipeline.py from v0.8.33 against this
manifest: a push to main yields cli-macos-arm64, cli-windows-x64 and
cli_verify_publish; the tag v1.8.0 yields the same in publish_release with
the publish job requiring both lanes and bound to the registry context.
ci-lanes.md now describes what runs (and that the lanes cover the Node
packages' build, tests and lint, not the binaries or the TUI), keeping the
0.8.32 gap list and proposal as history. The runbook marks the orb item
done, says a missing macOS or Windows resource class now holds a publish,
and no longer claims the Windows lane runs verify-binary.ps1.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The search-index test asserted that every search took under 50 ms and
the build under 200 ms. On the orb's cimg/node:22 medium container a
search took 51.5 ms and failed the release pipeline, though nothing was
wrong: the bound measured the machine, not the algorithm.
The test now runs the same queries (and the same index build) on an
instance with a tenth of the entries and asserts the large one costs less
than 40x as much. The index is a flat scan, so the observed ratio is
about 10x; a per-keystroke tree walk or a quadratic step would land near
100x and fail, so the guard still catches what it is for. Each side is the
least of several timed rounds, which filters scheduler and GC noise, and
machine speed cancels out of the ratio. A 500 ms absolute backstop remains
for local runs only (skipped when CI is set). Three full npm test runs
under nice -n 19 with four busy loops competing all passed (ratios
7.6x to 10.1x).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
qqq --version said "servers UI API >=1.6.0 <2.0.0" while qqq doctor said
"supports 1.6.0 to 1.8.0", which reads as if a 1.9.0 server were out of
range. The range is now built from UI_API_MIN_VERSION and a new
UI_API_MAX_EXCLUSIVE in version.ts, and doctor's ok, warn and fail details
all quote that UI_API_RANGE string. A newer-minor server (1.9.0) is
explicitly called supported while the warning about its new operations
stays. The comparison logic is unchanged: 1.6.0 and 1.8.0 are ok, 1.9.0
warn, 1.5.2 and a server without major 1 fail.
A test pins the range text, checks that --version and every doctor detail
use it, and keeps the old statuses.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
TRY-IT.md still introduced itself as "updated with every milestone" with
sections marked "M1 in progress" and "M3", which reads as unfinished on a
1.0.0 release page. The intro now says what it is (a walkthrough from a
source checkout against the sample server) and points to the README's
install section; the milestone markers are dropped. Commands and content
are unchanged, and the readiness gate still finds every one of them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1.0.0 ships on 2026-11-01, so the YYYY-MM-DD placeholder the runbook says
to fill in is filled. The entry claimed to be the release candidate's
"unchanged in content", which stopped being true with the installer's
stable-channel default and the fast-uri bump; those and the new
documentation are listed in a "Changed since 1.0.0-rc.1" section.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/ had no index, so a reader could not tell the runbook from the
historical plans. docs/README.md now describes every file under docs/.
The runbook, ci-lanes and 1.0.0 release notes still spoke as if rc.1
were pending and left the Scoop bucket undecided. They now name
QRun-IO/homebrew-tap and QRun-IO/scoop-bucket, state that the 1.0.0
version step is done on release/1.0.0, note what qqq-orb PR #165 changes
about publishing, describe the installer's stable default, and the
release notes list the Homebrew and Scoop commands. The generic rc-to-final
recipe keeps its -rc.N placeholders on purpose.
Two links in parity/next-feature-matrix.md pointed at files that exist
only in qqq-frontend-next; they now use absolute GitHub URLs. Its content
is otherwise untouched. Every relative Markdown link now resolves.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The installer took the newest cli-v* release including prereleases, so
after 1.0.0 the next release candidate would have reached curl users
while Homebrew and Scoop stayed on the stable channel. It now picks the
newest release whose "prerelease" is false; --pre (or QQQ_PRE=1) opts
into prereleases and --version still pins one.
--help printed nothing under curl | sh because it read the script back
with sed from "$0"; the help is now a heredoc. The Windows hint says to
add the QRun-IO scoop bucket before scoop install qqq. A --dry-run option
prints the resolved version and URLs, which made the change testable:
sh -n and shellcheck are clean, and dry and real runs against compact
and pretty-printed release lists and a file:// mirror pick 1.0.0 over a
newer rc, the rc with --pre, and ignore tag_name/prerelease text inside
release notes. The README install section documents the stable default.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The homebrew-tap profile's validator rejects a setup caller that runs on
tags, so the tap's .circleci/config.yml is now byte-for-byte the one in
QRun-IO/homebrew-qctl (tags: ignore). brew style flags a formula without
the frozen_string_literal magic comment, so it is added; the 64-hex zero
placeholders stay so scripts/update-packaging.mjs still fills them.
The formula and Scoop manifest placeholders now name 1.0.0 instead of
1.0.0-rc.1, and the packaging notes name QRun-IO/scoop-bucket, say that
the tap's CI executes only the Linux x64 binary (macOS downloads are
fetched and checksummed, not run), and that the bucket uses the
ScoopInstaller BucketTemplate Actions with Excavator auto-updating on new
cli-v* releases.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The repository had no contributor or security guidance and no .github
directory, so a 1.0.0 public release would give reporters no private
channel and contributors no statement of the checks, the signed-commit
rule on main or the license their work falls under.
Modeled on QRun-IO/qqq but adapted: npm workspaces plus Bun for the TUI,
the qqq-orb cli profile, Apache-2.0 (no AGPL text copied), conventional
signed commits. CODEOWNERS requests @KofTwentyTwo on every change.
Dependabot groups weekly minor and patch updates for the npm workspaces
and, through its bun ecosystem, for packages/tui's bun.lock; the
throwaway tui-spike is left out on purpose. Issue templates ask for the
qqq doctor build line, and route server and security problems elsewhere.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The CLI and client READMEs were about a hundred words each, and the
conformance package had none, so their GitHub Packages pages said almost
nothing. Each now covers purpose, install (including the read:packages
token GitHub Packages needs), a quick start whose code was run against the
local sample server, the API surface, auth modes, compatibility and the
versioning policy, with absolute links back to the repository docs since
relative links do not resolve on a package page.
The core README gains install and quick-start sections and rows for the
26 modules the index exports but the table did not list. The TUI README is
corrected, not rewritten: qqq ui is in the release binaries only, and a
checkout runs it with node packages/cli/dist/main.js ui after npm ci.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@qrun-io/qqq-ui-api-conformance had no homepage, bugs or keywords, so its
npm page would link nowhere, and the client was tagged "cli". All four
publishable packages now carry the same repository, homepage, bugs,
license and GitHub Packages publishConfig shape, with keywords that
describe each package. Versions are untouched; npm pack --dry-run shows
each tarball with dist, README.md and LICENSE and no tests or source maps.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The rc-era README led with a placeholder OAuth sign-in and mentioned
Homebrew and Scoop only as "prepared". For 1.0.0 the first screen has to
answer what qqq is, how to install it on every platform (curl installer,
qrun-io/tap, the QRun-IO scoop bucket, direct download, npm for the CLI
only) and what using it looks like.
The CLI tour now uses real output captured from the local sample server
(login, whoami, query with a filter, get, process run, export), the TUI
tour explains Home, the function-key bar, the / palette, F9 and the :
line, and new sections state compatibility, the versioning policy and an
index of every document. The Gatekeeper and SmartScreen notes are kept.
scripts/readiness.mjs still finds every documented command in the program.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fast-uri 3.1.9 fixes four URI parsing advisories (GHSA-w74x-v6f9-j3xg,
GHSA-wjr9-j795-jxcr, GHSA-xhq7-xmqj-769h, GHSA-333x-w4r6-6crp). It reaches
us only through ajv in @qrun-io/qqq-ui-api-conformance, so this is a
lockfile-only change, byte-identical to Dependabot PR #9. Landing it on the
release docs branch lets that PR be closed as superseded; npm ci is clean.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
All four packages go final together, with the exact workspace pins, so
the orb publishes them as latest. The CHANGELOG's rc.1 entry becomes
"## [1.0.0] - YYYY-MM-DD" (date to fill in on release day),
docs/release-notes/1.0.0.md is the release body with the final sequence
(tags v1.8.0 and cli-v1.0.0, gh release --latest, dist-tag latest), the
README, the CLI README and TRY-IT drop the rc version and @rc, and the
runbook says this branch is fast-forwarded onto main the day rc.1
feedback is accepted. node scripts/release.mjs --check is READY for
1.0.0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Found on the fix-branch sample (8bf6a5a66, private server on 8091): grid
cells showed REVEAL secrets and raw RENDER_HTML; they are now drawn like the
record view (masked, text, CHIP brackets). A WIDGET-adorned field with a
plain value shows the value. Create forms ask for a text primary key (the
archive table's file name) and leave out generated numeric keys. Parity
notes record what was verified live there: weekday criteria (QRY-085
pass), case-insensitive matching, AdornmentLab adornments, the composite
association, the locker refusal, uploads and downloads, sharing, saved
report setup, help. NAV-030 and PRC-020 stay blocked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/release-notes/1.0.0-rc.1.md is the user-facing release body (install
table, the Gatekeeper and SmartScreen notes, getting started, what is in
it, known limitations), with the exact rc.1 command sequence for James
in an HTML comment that GitHub does not render. release.mjs prints
--notes-file with that file when one exists for the version, else the
CHANGELOG section.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Quick search sends CASE_INSENSITIVE; a term starting with = matches exact
case. Guided filter text conditions (contains, starts, ends, like) get the
option; the condition menu offers Match exact case, shown as (exact case)
and printed as OPERATOR:CS in the grammar so it survives the round trip.
The palette ignores case; =Text asks for exact case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The draft 1.0.0 entry adds, for the CLI, home, find, apps show --widgets,
widget get --record/--run, report run, saved-report, share, login
--username for TABLE_BASED, table columns, api-call, widget-form, script
test, table get --copy, table update -e, labels in filters, case-
insensitive text matching by default (and that the server does not read
the option yet, finding 27) and refusals on DISABLED tables and
processes; for the UI, the bulk wizard, reports and saved-report editors,
sharing, the TABLE_BASED, PKCE and Auth0 sign-ins, widget actions,
dynamicForm and composite inputs, permissions, F1 help slots and the
large-table timings. Known limitations now name only jobs and Auth0, and
parity stands at 0 open CLI rows and 1 UI row. The README grammar table
and sign-in notes follow.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scripts/soak.sh drives the real TUI in tmux with random keys from the keymap
(F-keys, arrows, paging, Tab, Esc, Enter, single keys, : commands) and fails
on an exit or an error boundary, keeping the last 20 frames; nothing that
quits, saves, confirms a delete or exports is sent (QQQ_UI_NO_QUIT=1 ignores
a Quit chosen from a menu). The theme picker keeps a theme for every profile
or this one. README and TRY-IT: options, environment, per-screen keys, the
settings file, themes. SEC-030 is blocked on qqq-cli#10 and qqq#1359.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The base brought its own sharing module, a manageSession password sign-in and render-model
additions. The CLI now uses records/sharing.ts for qqq share, keeps its sessionId-cookie sign-in
as signInWithPasswordCookie next to the TUI's signInWithPassword, and the render model keeps both
sides: the web's wording and empty states from this branch, the TUI's pairs for the saved report
setup widgets (saved-report show still words them as the web does), its bar-chart default,
fieldValueList indents and block markers. Finding 28 is qqq#1359; SEC-030 also cites qqq-cli#10.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The navigation, widget and report rows were the last open CLI rows. The kernel render model now
follows the web's widget contracts (multiStatistics values, location, usaMap, generic, cronUI,
dynamicForm, pivot and filter setups, rowBuilder, every composite block, table extras, chrome,
empty states, the dropdown message), and a loader fetches widgets as ConnectedWidget does:
metadata first (unknown or denied widgets cost no request), parent widgets load their children,
data bag and script viewers read their records, dashboards load every widget once at once.
On top: qqq home, qqq find, apps show --widgets, widget get --record/--run; report run through
the report's process (input fields, GET /download) or the report route; qqq saved-report
(show, preview incl. through an application API, run, set filter/sort/columns, pivot, schedule)
and qqq share for any shareable record. A WIDGET-adorned filter field in a process takes the
filter grammar. Printed filters no longer mark case-sensitive matches (the TUI builds criteria
without options, and its footers must not change).
The sample has no reports, saved reports or editor widgets; stub-server CLI tests cover them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AUTH_0 signs in in the browser (authorization code + PKCE with the API
audience, code exchanged at the tenant, the token handed to manageSession)
and can end the Auth0 session on sign-out. F1 uses the kernel's help
entries (app, widget, instance, table, process, step) with readable places;
:helpkeys shows every help slot key. The grid footer shows the column's
help. scripts/perf-large-table.sh times a 10,000 x 40 table on the fake
large-instance server: first page ~100 ms, record 10,000 ~40 ms, column 40
~350 ms. Parity: TUI 330 pass, 67 n/a, 3 blocked, 1 todo (SEC-030: needs a
real Auth0 tenant).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
core: widgets/controls.ts (dropdowns, selections reconciled, params, date
picker values, export CSV); the render model draws dynamicForm, cronUI with
the record, usaMap, location, process, rowBuilder, setup widgets, image/
audio/icon/unsupported blocks, typed table cells, empty states, untyped
charts, single-block payloads and hides hidden alerts; PASSWORD values masked.
tui: WidgetView (v1 widget route, stored dropdown selections, denied
widgets not requested, parent widget children in grid or tabs, export,
process widgets, data bag versions, script revisions) on home, app homes and
record sections; saved report filter/columns/pivot setup editors (and the
API-versioned setup through the application API); WIDGET-adorned filter
fields in forms; dynamicForm editing; ad hoc widget buttons and Enter-
submit inputs; negative horizontal bars; disabled tables, processes and
apps explain the denial; capabilities gate new/edit/delete/copy; tables
without sections show their fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TABLE_BASED servers could not be signed in to at all. The server's TableBasedAuthenticationModule
accepts HTTP Basic credentials on any route, inserts a session and answers with a sessionId
cookie; qqq login --username now does that once (password asked, or QQQ_PASSWORD), keeps only
the session and sends it as the sessionId cookie, which the typed client could not do, so the
CLI wraps its fetch. A refused password stores nothing; logout ends the session.
AUTH_0 servers were refused by the native sign-in discovery. The client now builds the sign-in
from the tenant, the native (or SPA) client id and the audience, which Auth0 needs to issue a JWT
the server can verify. No AUTH_0 server exists to run it against (finding 28).
DISABLED tables and processes come in the metadata with their permissions false; the CLI lists
them with the reason and refuses query, writes, copy (insert needed), bulk commands and process
run before any request, as the web explains the denial.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
qqq ruled that CONTAINS, STARTS_WITH and ENDS_WITH are case-sensitive by design (finding 11,
qqq#1344) and that clients opt in per criterion with CriteriaOption.CASE_INSENSITIVE. The
grammar and quick search now add the option to every text match; ~= , OPERATOR:CS and
--case-sensitive ask for a case-sensitive match, and printed filters mark those with :CS.
The option does not reach the backend over v1 yet: QFilterCriteriaDeserializer ignores
options and the spec does not declare them (finding 27), so matching stays case-sensitive
until the server reads it. Finding 26 records the bulk mapping suggester problem the kernel
works around (it was cited as finding 11 by mistake).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
core: records/sharing.ts (share processes, audience, owner rule) and
signInWithPassword (TABLE_BASED via manageSession with Basic credentials,
nothing kept on refusal). cli: exports browser-login for the TUI.
tui: share dialog on shareable records and saved views (Z, :share);
TABLE_BASED username and password prompts; OAUTH2 sign-in in the browser
(authorization code + PKCE) with the device flow as the alternative;
sign-out ends the server session or the provider session; a 401 mid-use
opens sign-in on top and returns to the refreshed screen; :views.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CLI column moves to 232 pass, 2 blocked (finding 19 for variant writes, finding 24 for
weekday criteria on H2) and 1 partial (NAV-028, navigation group next). Rows the sample cannot
exercise say unit-tested or stub-server tested in their note.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Closes the record, query and process gaps the parity matrix listed. Each capability lands in
the kernel with tests and a thin command on top:
- possible-value labels in --where are resolved to ids, as the web filter sends them
- table columns keeps the query columns per table in the config the TUI column chooser uses
- the variant chosen for a table is remembered per server (--variant - forgets it)
- ad hoc composite widgets: conditional blocks, INPUT_FIELD blocks asked as step fields,
BUTTON actionCodes offered as choices
- HTML fields show as text; table update -e edits HTML and code in $EDITOR
- CHIP values carry their color and icon (colored on a terminal; tables measure ANSI-free width)
- table get --copy, table api-call (the API playground), script test, table widget-form
(a dynamicForm on a record form, saved in its merged JSON field)
Stub-server CLI tests cover what the sample cannot exercise (variants, saved views, audits,
application APIs, script tests, dynamicForm).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bulk insert and bulk edit with a file (file navigator upload, column mapping
grid with typed defaults and field help, value mapping with the picker,
review with optional validation, result), bulk edit (switched-on fields) and
bulk delete; saved bulk load profiles (yours and shared, owner-only update
after a confirm listing the changes, delete); the upload template; table
actions list the bulk actions with their CLI commands. Verified live on the
sample: 2 person rows loaded from CSV, bulk edited, bulk deleted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
table bulk-insert, bulk-edit, bulk-delete, table actions and
bulk-profile join the draft 1.0.0 entry and the grammar table; the
known limitation "bulk load is not in the CLI" is gone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The bulk rows (PRC-016, 029-034, 046, 058-061, QRY-031/032/034) are verified live against the
sample, except saved profiles, which the sample does not offer: those are covered by a stub-server
CLI test. The matrix script gains a blocked status (a note must cite a server finding) so the
release gate can tell a server limitation from unfinished work.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A report with a process runs through its screens and saves its file on
completion; otherwise the format is chosen and the streaming report is saved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Record view value columns start from the field set (DATE_TIME 30, dates,
numbers, text length) as well as the first record, so an empty first
record no longer cuts later values; still fixed while stepping.
Parity TUI column: the partials resolved: passes (live or unit-tested, said
in each note), browser-only n/a with the reason (icon fonts, logos, URL
state, rich-text and code editors), and blocked on server findings 13
(cancel keeps the run) and 21 (no search provider). Remaining partial: report
runs, the 10,000 x 40 budget, ad hoc widget buttons, bulk load (parked),
OAuth authorization code + PKCE.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The entry now describes qqq ui (Home, F9 command center, the grid,
records, uploads and downloads, processes, developer mode, tags,
dashboards, terminal checks) and the CLI's developer-mode and tag
commands, in user-facing terms, and says final waits for parity. It is
the draft of the 1.0.0 entry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The orb (PR #165) publishes all four packages as rc when any of them is
a prerelease and as latest only when all are final. The readiness gate
gains a package-versions line that refuses a final CLI version while
client, core or conformance is still a prerelease; release.mjs derives
the dist-tag from all four. The runbook's version table, the rc-to-final
step and the publish step follow the rule, and its signing step now
uses sign-macos.sh and sign-windows.sh.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Grid: Ctrl+A opens the selection menu (this page, the full result, the
first N, clear) and the bulk bar runs processes on that selection; pinned
columns stay left while scrolling; DATE_TIME criteria typed in the local
zone go out in UTC (kernel filterDateTimesToUtc); l opens the record a
possible-value cell links to; the empty state offers F7.
- View settings: / searches the column list, columns grouped by table and
join with shown counts, * marks what differs from the default.
- Saved views: rename and update with the current query.
- Record view: child lists three levels deep (a pet's notes under the pet),
section help, CHIP values in their color, WIDGET-adorned fields drawn as
widgets; F1 shows table, section and field help for the screen's roles
(view, edit, query) and process and step help; forms show field help.
- App dashboards lay widgets out by gridColumns side by side on wide
terminals.
- Process steps page their RECORD_LIST (n / p) through getProcessRecords.
Tests: a fake acceptance table and process cover CHIP, WIDGET, help by role,
a form adjuster, audits, column statistics, developer scripts and logs,
table APIs, a section widget with the record context, a composite
association, a named step widget, record paging, back step and a
DOWNLOAD_FORM; and the grid selection, pins, UTC filter and cell link.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
sign-macos.sh signs the darwin binaries with a Developer ID under the
hardened runtime with Bun's documented entitlements, notarizes them
(keychain profile or App Store Connect key) and recomputes SHA256SUMS;
sign-windows.sh Authenticode-signs the .exe with osslsigncode. Both read
everything from environment variables, exit 0 with what is missing when
it is not set, and have --dry-run. Checked here: both no-op messages and
dry runs; an ad-hoc hardened-runtime signature with the entitlements,
after which verify-binary.sh against the sample server and the qqq ui
tmux smoke still pass; and a real osslsigncode signature with a
throwaway self-signed certificate in Docker, which verifies against
that certificate and changes the recorded checksum. Notarization and a
real Developer ID or Authenticode certificate were not available.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bulk load was the largest parity gap: the CLI could not load a file or change many records at
once. The kernel gains the bulk load model (table structure, v1 profile, mapping documents, the
server's suggestion cleaned per finding 11, mapping checks, diffs, upload templates) and
runBulkProcess, which drives the bulk processes through the runner with one callback per screen
(edit form, column mapping, value mapping, review), so the TUI can reuse it. Profile values go
as flat strings (fieldListJSON), cleared bulk-edit fields as "" (finding 23), and the file is
sent again after Back to the upload. BULK_LOAD_PROFILE_FORM no longer counts as an input, so the
result step is the last screen.
The CLI adds table bulk-insert, bulk-edit (--set/--clear, or --file with --key), bulk-delete,
table actions and bulk-profile list|show|save|rename|delete. Mapping comes from the server's
suggestion, a --mapping document (YAML/JSON), --column/--default, or -i prompts; the review is
shown, validation runs by default, and loading asks unless --yes (exit 2 without a terminal).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scripts/readiness.mjs reports what a final release still lacks: parity
rows that are partial or todo in the CLI or TUI column (a blocked row
must cite a finding or issue), server findings without an issue link, a
missing CHANGELOG entry for the version, and qqq commands quoted in
README.md and TRY-IT.md that the program (createProgram from the built
CLI) does not have. release.mjs refuses a non-prerelease version until
the report is clean, prints it as information for a prerelease, and
--check prints only the report. Today: 122 CLI and 152 TUI rows open,
everything else clean.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/release-runbook.md goes from a green branch to 1.0.0 published:
James's checklist first (orb release, CircleCI plan, Developer ID and
notarization, Authenticode, the tap and bucket repositories, the publish
token, the go decision, each with what happens without it), what is
not verified, then version bumps, release.mjs, signing, tags and the
orb publish, the GitHub release, tap and bucket updates, the smoke after
publishing and recovery. The README and CHANGELOG describe the one
binary with qqq ui, the Gatekeeper prompt and the xattr workaround. The
orb proposal record gains the rc-versus-latest dist-tag change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A prerelease keeps rc and --prerelease; 1.0.0 prints --tag latest,
--latest, and the dist-tag commands that move latest while the orb
still publishes everything as rc. The printed packaging step names the
tap's formula path and the signing step.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Version with "binary with ui", completion for four shells, qqq ui
--check naming the build, and with a server: login, doctor, whoami, a
query and a process run, in a temporary profile (file keyring, or the OS
keyring removed again). Run here on darwin-arm64 (both keyrings), in
Debian amd64, and the .ps1 under PowerShell 7 in a Linux container, all
against the sample server; never on Windows. The proposed lane YAML now
calls them and still validates; ci-lanes.md says Windows x64 and macOS
x64 stay unverified until the lanes run.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/packaging/homebrew-tap/ is the whole tap repository (formula,
README, LICENSE, qqq-orb homebrew-tap profile); the formula moves there
as the single source, installs only bin/qqq with completions, says qqq ui
is bundled, and its test fails on a binary without the UI. The binary is
chmod 0755 before install: Homebrew downloads arrive without the
execute bit and the completion step runs the binary (found by installing
the formula in the homebrew/brew container, where it then installed,
passed brew test and wrote the three completion files). The Scoop
manifest describes the with-UI binary and notes the ConPTY requirement.
The packaging README covers Gatekeeper without notarization and the
xattr workaround, SmartScreen, and what is still unverified.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Exposed joins (QRY-020/021, REC-064): grid columns and filters take
joinTable.field (labels "Join: Field"); the kernel ResultCursor sends
the planned joins with query and count and reads the distinct count for
many-side joins ("3 rows (2 distinct)"); record sections show fields of
readable exposed joins through a joined query for the record.
- Quick filters (QRY-080/081): the filter dialog starts with the table's
quick filter fields (defaultQuickFilterFieldNames, else T1) and an add
row whose fields are remembered per table.
- Weekday criteria (QRY-085): the kernel grammar reads weekday(field) IN
1-7 or day names into a WeekdayOf fieldFunction (DATE_TIME fields get
WeekdayOfDateTime with the instance's arguments); the builder offers day
of the week is any of / none of. The sample rejects it: server finding 23.
- Bulk paste (QRY-088): list values accept pasted text split on commas,
tabs, semicolons and new lines, choice labels resolved to ids, with the
instance's bulkAddFilterValues help; bracketed paste reaches the focused
editor everywhere.
- First N (QRY-033): table processes run on the selection, the cursor
record, the whole result, or its first N records.
- Scanner screens (PRC-044): no heading or step list; Enter submits.
- Counted quick views (QRY-094): doCount views show their counts.
- Variant (REC-063): records and forms opened from a variant grid read
with tableVariant.
- The grid shows a failed query instead of keeping the previous rows.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The qbit-collaboration fix for #52 showed the presence qbit already sent
an empty viewers list; qqq core's SSE serializer removes it. Recorded so
the TUI's presence handling and the core fix point at the same cause.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The TUI batch (fixed-height rows, uploads and downloads, sort, process
screens, saved views, variants), so the with-UI binary carries it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A first 'build' line names the version, the install (which binary, with
or without the UI, commit; or the npm package or a checkout), the
runtime (the Bun embedded in a binary, or Node) and the platform, so
support can read it from one paste. qqq --version adds the runtime too.
doctor's minimum UI API now comes from version.ts instead of a second
constant.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
James's decision: one qqq binary per platform with the UI is the release
artifact. npm run binaries and release.mjs bundle the TUI unless
--cli-only. Both entries now start the built CLI (packages/cli/dist),
the module the TUI imports as @qrun-io/qqq-cli/program, so the CLI
program is in the bundle once (darwin-arm64: 100.8 -> 100.5 MB; the rest
of the 16 MB over the CLI-only build is React, OpenTUI and its native
library).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Process step forms show DATE_TIME inputs in the local zone and send UTC
instants (kernel datetime); processes launched from a variant grid carry
tableVariant through the runner.
scripts/parity-matrix.py: the TUI column re-scored against what is built
now. Priority families (NAV, PRF, PRC, QRY, REC, REL): 150 pass, 44
partial with a reason, 13 named todo, 42 n/a. All rows: 180 pass, 49
partial, 103 todo, 69 n/a.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
q quick-searches the visible text columns (kernel quickSearchFilter, ANDed
onto the filter with combineWithQuickSearch; Esc clears). S shows column
statistics of the column under the cursor with the current filter (kernel
runColumnStats; the server's refusal when the table has no QUERY_STATS).
F9 › Saved views lists, opens (filter, sort, columns via viewToQuery),
saves the current query and deletes views (kernel saved-views). A table
with variants asks for one before the first query, sends tableVariant on
query and count, and dismissing the prompt queries nothing.
Tests: quick search criteria on the wire and Esc, the variant prompt and
the tableVariant parameter. The sample has no QUERY_STATS, saved-view or
variant table, so those were seen live only as their refusal messages.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
James approved filing the 22 findings; one issue per finding except the
archive-table one (already open as #784), the two record-search ones
(one root cause, #1343) and the presence one (qbit-collaboration#52).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The wizard renders each step's screen blocks from the kernel runner (help
content, HELP_TEXT, HTML, VIEW_FORM values, RECORD_LIST records, named and
ad hoc widgets, DOWNLOAD_FORM files) above the form or result; steps
without input are shown through the runner's present callback. F4 goes
back to the backend back step when the step has one; w saves a DOWNLOAD_FORM
file through the save dialog; min and max input records are checked before
init (kernel checkInputRecords).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Forms: BLOB and FILE_UPLOAD fields choose a file in the file navigator and
the record is sent as multipart (create and edit). Process steps hand the
chosen files to the kernel runner through its new stepFiles option, sent
with that step. Record view: a BLOB stored in the record saves through the
save dialog (Enter on the field, or the actions list); child lists offer
Save entries for their BLOB and FILE_DOWNLOAD fields. Verified live on the
sample's Field Lab: a create with blobValue uploaded from a file, then the
value saved back byte for byte.
Grid view settings choose the sort (field and direction), kept per table and
applied when the grid opens. The filter builder offers the operators of the
field type, now with NOT_EQUALS_OR_IS_NULL, IS_NULL_OR_IN, IN on dates and
EQUALS on booleans, and More operators… for the rest. When the column layout
is frozen, text columns take spare width up to 40 cells so later pages have
room before values are cut.
Tests: filtered export (filter and visible columns sent, file written), a
file field uploaded as a multipart part and a BLOB saved from the record
view, runner stepFiles. scripts/smoke-widths.sh checks page 1 against page 2
in tmux on the fake large instance.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Publishing @qrun-io/qqq-core (blocking a usable CLI package) and opt-in
macOS and Windows lanes, written as a message for James with the
cli_publish.sh diff; the Node 22.12 row is green after the prompt fix.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
release.mjs passes --with-tui to the binary build. docs/packaging
records the three ways tried to ship qqq ui (one binary with the UI, a
second qqq-ui binary, npm with Bun), the sizes, what was smoke-tested
and what was not, and recommends one qqq binary with the UI bundled.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scripts/binary-with-tui.ts is a Bun-only entry that registers the
bundled TUI and starts the CLI, so the Node build is untouched. The
build installs OpenTUI's native package for every target (bun install
--os='*' --cpu='*' in packages/tui), pins OPENTUI_LIBC per Linux libc
and defines QQQ_BUILD_UI, which qqq --version prints as "with ui".
Source maps that bun writes beside each binary now go to
dist/sourcemaps/, so the release glob dist/qqq-* no longer uploads them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
In a binary or an npm install, qqq ui printed "looked for
/tui/src/main.tsx". It now says the TUI is not in this installation
and names the two ways to get it: a qqq binary built with the UI, or a
source checkout with Bun. A binary that bundles the TUI registers
globalThis.__qqqRunTui, and qqq ui hands over to it in-process with the
same arguments it would pass to bun run.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On Node 22.12, readline/promises leaves a pending question unsettled
when Ctrl-D closes the interface, so the prompt tests (and a real
prompt) hung until the event loop drained; that is the orb image. The
question now also settles when the interface closes: a usage error for
end of input, the caller's abort when it aborted. Ctrl-C still reaches
SIGINT (process run cancels, exit 130); checked on Node 20.20, 22.12
and 24.22 and with a real Ctrl-C and Ctrl-D at the clonePeople review.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cause: OpenTUI text rows default to flex-shrink 1 and word wrap. When any
row is wider than its box it wraps, the column overflows, and yoga shrinks
other rows to zero height, so lines in the middle of a screen disappeared
(the Home application card's counts line in the in-memory test, and any
screen in a real terminal whose content overflowed). Every text row now has
flexShrink 0 and wrapMode none, and panels, dialogs and the frame clip with
overflow hidden, so an overlong line is cut at the edge and extra rows are
cut at the bottom. A layout test pins it; the Home test asserts the frame
again instead of the card data.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Developer mode and the TUI batch (Home screen, search-first F9, the
metadata index, audits, tags and developer screens).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scripts/install.sh picks qqq-linux-<arch>-musl on Alpine, so a release
without them would leave those hosts with a 404.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
core: Collaboration.tags (vocabulary search), createTag, recordTags and
applyTags (add/remove in one request), with fake-transport tests.
cli: qqq collab tags list|add|remove|create (ids or labels).
tui: with the collaboration extension, a record shows its tags under the
header; t (F9 Tags…, :tags) opens the tag editor: remove with Delete, add
from the vocabulary searched as you type, create a new tag inline (a
suggestion on approved-only tables). Without the extension nothing shows
and :tags says it is not installed. The UI API has no rename or delete of
tags and no tag filter criterion, so those are not offered.
Verified live on the qbit-collaboration dev server (port 8098, separate
config dir, stopped afterwards): CLI list/add/create/remove, the TUI tags
line, the editor search, adding tui-test from the TUI.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The README now leads with what qqq is, how to install it (binary,
installer, npm), the first sign-in and the noun-verb grammar, then
points at TRY-IT and the docs. The CHANGELOG's 1.0.0-rc.1 entry sums
up the M1 work since 603c090 and the hardening review.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Records the orb profile as verified in Docker (Node 22.12 image, no lint,
core never published, one platform), the Node 22.12-only prompt test
failure, and a proposal for macOS arm64 and Windows x64 lanes: an orb
change with a .qqq-ci.yml opt-in, or a second CircleCI pipeline whose
YAML validates with the CircleCI CLI. .circleci/config.yml is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Record view: A opens the audit history (kernel records/audits; newest
first, who, when in the local zone, what, and the changes; the refusal text
when the table has no audit source), D the developer screen (kernel
records/developer: the server's developer panels drawn from blocks, the
associated scripts with revisions, show code, run logs, store a new version
from $EDITOR with a commit message), a the actions list (the F9 Context
entries). On a grid, a lists actions and D shows the table's application
APIs with their OpenAPI documents (view or save through the file dialog).
:audits, :dev and :actions do the same; F9 lists them.
The F-key bar shows the global label of a key that does nothing on the
current screen, dimmed, and switches to short labels below 100 columns.
Esc then a digit (Alt+digit) and Ctrl+digit act as F1..F10 for terminals
that take the F-keys. The terminal check adds the macOS function-key note
(and iTerm2's own F-key bindings).
Verified live on the sample: developer panels (Record Security, ESB
Events), no associated scripts, no table APIs, no audit source (refusal
shown).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Home is the counterpart of the web dashboard: greeting, connection and
counts, recently viewed records, quick actions (runnable processes, new
records, favorites with collaboration), application cards in a grid, 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, arrows inside, Enter opens, v orders the panels.
Large instances: the store builds the kernel metadata index once per
document; the palette and F9 show recents, commands and counts first and
search the index as you type (a page at a time, more on scrolling); the Apps
tree opens collapsed and filters as you type; app dashboards load widgets
and counts only as they come into view, four requests at a time, cancelled
when the screen is left. scripts/fake-big-server.ts serves a generated
1000-table, 500-process, 3000-widget instance.
F9 is a stack: entries that open dialogs leave it underneath and come back
to the same entry with refreshed states (the theme entry opens a picker);
navigating entries close it.
Grid column widths are computed once (metadata, view settings, first page)
and kept while paging, sorting and filtering; record value columns and child
list widths are kept while stepping records.
Tests: fixed widths across pages with longer values, the F9 stack with the
theme picker, Home panels, and F9 and palette timings on the generated
large instance (about 40-60 ms including the 30 ms settle).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The TUI is neither published nor bundled into the qqq binaries yet, but
it builds on the same kernel, so a release that breaks it should stop.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brings in the TUI package and the hardening review (createProgram in
program.ts, qqq doctor, the golden-output suite). Resolutions: main.ts
from the base; program.ts prints versionText(), whose second line now
starts with "UI API client <version>" as the golden test expects and
adds the supported server range; the CLI package keeps the release
metadata with the base's exports and its new conformance dependency,
and conformance goes to 1.6.0-rc.2 because the CLI now ships against it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Exports getRecordDeveloper, listDeveloperPanels, listTableApis,
getTableApiDocument, storeScript and getScriptLogs for both heads;
panels render to the neutral render blocks. Revision code is read from
scriptRevisionFile, as the web UI does.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brings in the TUI stream (program.ts with createProgram and runCli, the io
object, qqq ui, session/login, local-state, filter completion, the result
cursor, the file navigator and env/doctor). Resolutions:
- main.ts is the base's thin entry; it still awaits the 0.1 credentials import.
- program.ts carries the option fixes (--version only, --server without -s)
and registers qqq doctor.
- One set of ui helpers: the TUI's. readUiSettings/writeUiSettings are gone;
qqq config set|get download-dir, upload-dir and download-mode use
readFilePreferences/writeFilePreferences (ui.files), and the new
qqq config set|get theme uses readTheme/writeTheme. readConfig keeps the
corrupt-file refusal and unknown sections; the profiles test covers files,
theme and an unknown section together.
- qqq doctor's terminal rows come from checkEnvironment, with formatReport
under the table; --fix asks before each applyLiveFix and calls
appendTmuxConf only after a y typed at a terminal.
- runCli and the prompts coexist (stdin is not a terminal in runCli, so
prompts refuse; direct stdout writes are captured), tested in a child process.
- @qrun-io/qqq-ui-api-conformance is a dependency of the CLI package.
- The parity matrix is regenerated; docs/review-2026-10-31.md records each
merge decision.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
metadata/index.ts builds one index from the cached GET /metaData (apps,
tables, processes, reports, widgets with label, name, app path, hidden
flag) and searches it by exact, prefix, word prefix, substring and
subsequence match, ranked then by label, paged with a total for more.
generateMetaData makes a document of any size; the test indexes 1000
tables, 500 processes, 2000 widgets and 300 reports (0.57 MB: parse 3 ms,
index 4 ms, slowest search 9 ms on this Mac).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/review-2026-10-31.md lists each finding of the fresh-eyes review, its
severity and the fix (or why it was left). TRY-IT.md was run block by block
against the sample server and corrected: placeholders written as commands,
missing upload files, record ids the shared data no longer matches; it now
documents the option names shared by every noun, the footer, the exit codes
for a no, Ctrl-C and gateways, the download directory and qqq doctor. The
parity matrix notes REC-022 (BLOB upload and download) as verified and the
REVEAL masking in lists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"(3 values; of 5; --limit for more)" read badly; it is now
"(3 values; 5 in all; --limit for more)".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
node --test only expands glob patterns itself from Node 21. With the
pattern quoted, Node 20 looked for a file literally named
dist/test/*.test.js and every workspace's npm test failed, although
the packages declare engines node >=20. The orb's Node 22 image hid it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scripts/install.sh picks the binary for the machine (musl on Alpine,
native arm64 under Rosetta), verifies it against the release's
SHA256SUMS and installs to ~/.local/bin. qqq.rb and qqq.json follow the
qctl tap and bucket shapes; scripts/update-packaging.mjs fills their
version and checksums from the uploaded release so nobody pastes sums
by hand. The formula writes its own #compdef zsh file because the
output of qqq completion zsh is an eval script, not an fpath file.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Quit ends the program after the unsaved-form or running-process warning.
Reset closes every dialog and screen above Home after the same warning,
refreshes the metadata, and returns to Home with the Apps tree focused,
keeping the session and profile; also :reset, :home and the F9 command
center. Esc cancels. Test, smoke script and docs updated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
packages/tui/README.md: running it, keys, screens, the : commands, the local
settings, the keys terminals and tmux may take (with the alternate_on
rebinding), tests and the code layout. TRY-IT.md replaces the spike section
with the qqq ui section. scripts/parity-matrix.py gets a TUI status table
and a TUI summary; feature-matrix.md regenerated (TUI: 118 pass, 25
partial). Server findings: recordSearch advertised with no provider, and
the untyped multiStatistics group (statisticList).
Also: -o -field in : commands and qqq ui routes, ASCII title separators,
short F-key labels at 80 columns.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
qqq doctor checks the profile, that the server answers GET /version, that its
UI API version is in the supported range (1.6.0 to this client's 1.8.0, major
from supportedMajorVersions), the authentication type, that the stored sign-in
or QQQ_TOKEN is accepted, the installed extensions and their minimums, the
keyring kind and the metadata cache age; --conformance adds the read-only
conformance checks (the conformance package, loaded when installed). Exit 0 when
everything passes, 1 with the failing checks in the footer.
The terminal half calls the kernel's checkTerminalEnvironment() by name, so it
reports "skip" on this base and the report (tmux bindings, colour, size, and
--fix with its two confirmations) once the TUI branch's module lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clean tree, free tags, npm ci, the orb's generate-drift, build and test
steps, lint, npm pack dry runs and the binaries; then it prints the
signed tag, npm publish and gh release commands. It never tags,
pushes or publishes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scripts/build-binaries.mjs (npm run binaries) writes
dist/qqq-<os>-<arch>[.exe] for macOS arm64 and x64, Linux x64 and arm64
(glibc), Windows x64 and, with --musl, Linux musl, plus SHA256SUMS. It
refuses to build when the compiled-in operations index no longer
matches the OpenAPI document or the CLI version constant disagrees with
package.json, stamps the commit and target into qqq --version, and
smoke-runs the host binary. x64 targets use the baseline runtime so they
run on pre-AVX2 CPUs and under Rosetta or QEMU.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Checks the CLI's compiled-in version against its package.json, the
client's UI_API_VERSION against the document's info.version, and every
workspace dependency against the workspace version, so a coordinated
bump cannot leave one behind.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CLI and core go to 1.0.0-rc.1 and the client to 1.8.0-rc.2, with the
workspace dependencies pinned to them. Every published package now has
repository, homepage, bugs, engines (Node 20+), publishConfig, explicit
exports and a files list that leaves out compiled tests and source
maps. The repository LICENSE is copied in on prepack because npm only
packs a LICENSE from the package root. core had no publishConfig, so a
publish would have gone to the public registry; it now targets GitHub
Packages like the others. The CLI and client packages get the README
their files lists already named.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The version moves to packages/cli/src/version.ts so the single-file
binaries can stamp the git commit and target (QQQ_BUILD_SHA and
QQQ_BUILD_TARGET defines) without touching the npm build, and so the
supported server range (UI API >=1.6.0 <2.0.0) is stated next to the
version a user reports.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A node:test suite that spawns the built CLI against a fake QQQ server
(node:http) answering with responses recorded from the sample server, in a
throwaway config, cache and file keyring with TZ=UTC and no terminal. It covers
table list, describe, query (table, json, csv, text, yaml, --query, --utc),
count, get, create, update (contingent fields), delete with and without --yes,
export to the download directory, process describe and run (synchronous and a
polled job), widget get and --csv, api, extensions, an installed and a missing
extension noun, exit codes for 400, 401, 403, 404, 500, 503, a refused token and
an unreachable server, bad numeric options, the no-terminal refusals, a corrupt
config.json and the 0.1 credentials import. About 4 s; UPDATE_GOLDEN=1
rewrites the golden files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Short options that collided: the global -s (--server) took every -s meant as
--set (table create, copy, update, prefs set: "Error: Invalid URL"), the
global -p (--profile) took -p meant as --path (qqq api and every generated
extension verb), and commander's -V (--version) took table query -V (--view),
printing the version and exiting 0. --server and --path have no short form now,
the version is --version only; collab presence --watch lost -w (it is --where
everywhere else) and saved-view save takes -l, --limit like table query.
table update read nothing of the record without -i, so a contingent field
could not be changed ("City: set State first" although the record has one)
and possible values were not narrowed; it now starts from the record, and a
changed dependency also sends the dependents it cleared.
One renderer for record values: query rows, child lists and joined fields go
through the record view's cell mode (REVEAL masked, with --reveal on query and
children), and the duplicate valueOf helper is gone. One DATE_TIME path:
audits, collab comments and people, ESB counters and messages, job logs and
tail now show instants in the zone (or raw with --utc) instead of slicing UTC
text. One help path: table get --help-content and process describe print what
help show prints. One footer: (N records; ...) through countFooter, with
plurals (no more "(1 results)").
One download path (writeDownload): table download and export, report run,
widget --csv and api --out write through saveDownload, take - for stdout, and
default to the download directory (qqq config set|get download-dir, upload-dir,
download-prompt, kept in the ui section the TUI shares); process --out-dir
defaults to it too.
Also: numeric options (--limit, --skip, --page-size, --timeout, ...) are checked
(exit 2) instead of sending NaN; missing files named by --file, --form @file,
--bytes or a JSON file argument are usage errors; table children and download
take --variant; association metadata errors other than 403/404 are no longer
shown as "unavailable"; tail ends quietly with 130 on Ctrl-C; answering n to
Submit? is "Not submitted" with exit 0, as "Not deleted." is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
core/env/doctor.ts finds tmux root bindings that take keys the TUI needs
(PgUp/PgDn, F1-F10, Home/End, Esc, / ? :), checks alternate-screen, TERM,
true color, the locale and the size, and names the terminal program with
its known traps (Terminal.app F-keys, iTerm2 page keys). Each tmux finding
carries a live fix (bind-key -n KEY if-shell -F "#{alternate_on}" "send-keys
KEY" <the old binding>, so full-screen programs get the key and the shell
keeps its behaviour) and a ~/.tmux.conf line appended in a marked block only
after a y. Choices are kept in the config file's ui.doctor; tests run on
fake tmux output.
qqq ui runs the quick check before the renderer starts and asks only about
warnings not dismissed (never), --check prints the full report, --no-check
skips it; a non-UTF-8 locale switches to ASCII glyphs. The F9 Help section
and :doctor show the report in a dialog.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Help content had three extractors (helpContentText for help show, helpEntryText
in the record view, helpText on process screens) that disagreed: an empty
contentAsText hid the help on process screens, MARKDOWN without rendered HTML
came out differently. helpContentText is now the rule and the other two use it;
the role filter takes several roles (the view screen's).
valueText gets cell mode: the record view's adornment rules (REVEAL masked,
CHIP, ERROR, DATE_TIME in the zone) without link targets, download hints or
tooltips, so record lists can use the same renderer as the record view instead
of showing REVEAL values in the clear.
fetchChildren takes the table variant for child tables on a variant backend.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
runProcess looped at most 200 times and then fell through to the normal return,
so a server that kept answering with a nextStep looked like a successful run.
It now throws a ProcessError naming the step (MAX_STEPS).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Context.confirm (delete, saved-view delete) and the variant chooser opened their
own readline without a SIGINT listener, so Ctrl-C at "Delete? [y/N]" failed
the question with a bare AbortError ("Error: The operation was aborted",
exit 1) instead of exiting 130. Both now go through prompt.ask, which passes
Ctrl-C on as SIGINT, turns Ctrl-D (end of input) into a usage error, and takes
an input stream so it can be tested without a terminal.
Exit codes: 502, 503 and 504 are transient (4), like a refused connection,
rather than 70; any fetch TypeError naming a network failure is 4; a missing or
unknown path parameter of qqq api is a usage error (2, OperationInputError)
instead of 1. callOperation reads an empty JSON body as no data instead of
throwing a SyntaxError.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
importLegacyCredentials fired the keyring writes without awaiting them
(void secrets().set(...)) and renamed credentials.json straight away: a
keychain that refused the write lost the 0.1 sign-in, and the rejection went
unhandled. The import is now awaited before any command runs, each write is
awaited, and the file is renamed only when all of them succeeded (otherwise it
stays and a warning says why).
getLogin turned a stored sign-in it could not parse into "not signed in",
which surfaced later as a puzzling 401. It now says the sign-in is unreadable
and how to replace it; logout still removes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
readConfig treated a config.json it could not parse as empty, and the next
upsertProfile wrote that back: one stray comma lost every profile. FileStore and
DpapiFileStore did the same to secrets.json, so saving one sign-in wiped the
others. Both now throw (ConfigFileError, SecretFileError) and change nothing.
readConfig also dropped every section it did not know; it now keeps them, and
the ui section (defaultDownloadDir, defaultUploadDir, downloadPrompt) gets
readUiSettings/writeUiSettings for the CLI and TUI to share.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bun test in packages/tui (npm run test:tui at the root): the whole TUI
against a fake server in the test renderer: Esc from record 4 lands on row
4 and after stepping to record 7 lands on row 7; server paging, sort and a
:filter; a contingent form where State waits for Country and narrows;
the process wizard through an asynchronous job to its summary; F10 asks.
Plus unit tests for column layout, the line editor under a key burst,
command-line splitting and theme tokens.
packages/tui/scripts/smoke.sh drives the real TUI in tmux against the
sample server through fourteen screens and keeps the captures.
The kernel's multiStatistics reading accepts statisticList, the spelling
the sample server sends. Lint covers packages/tui.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
core/files/navigator.ts: directory listings (directories first, sizes,
dates), path expansion and Tab completion, new folders, and the local file
preferences (default download and upload folders, last used, ask or save
silently) in the config file's ui section; tests on a temp directory.
The TUI's file dialog draws them: typing filters, Backspace goes up, a path
line with completion, h hidden, ~ home, Ctrl+N new folder, and a save
variant with a sanitized name that asks before overwriting. Exports and
record file downloads use it, or save silently to the download folder when
that is the preference; the F9 Settings section sets the folders and mode.
Ctrl+F / Ctrl+B page everywhere and Space / Shift+Space page lists, for
terminals and tmux setups that take PgUp/PgDn. Fill-the-width spreads over
the visible columns.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The record view renders values through the kernel's record-view module
(adornments, REVEAL masking, links, FILE_DOWNLOAD saves via saveDownload),
child lists through associationGroups, childListFields and childFilter, and
Esc from a child record lands on the child last viewed. Forms copy with
copyRecordValues and edit DATE_TIME in the local zone (utcToLocalInput,
dateTimesToUtc); grids show DATE_TIME in the local zone. Extensions use the
kernel ExtensionRegistry; the presence slot is fed by the kernel
Collaboration module when the extension is installed. Go to by key uses
gotoKeys and findRecordByKey. Breadcrumbs follow the app path. The F9 menu
scrolls its columns; saved views, column statistics and audits run their
CLI commands in the output pane. Palette says when the server has no record
search provider.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Verified in tmux against the sample server: sign-in with the stored
profile, home, the person grid, record 4 with PgDn to record 7 and Esc back
to row 7, the guided and advanced filter (completion, live grammar, CLI
equivalent), and a placeAddress create with country, state and city
narrowing, saved and shown as the new record.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Keeps every kernel export; the CLI entry stays a thin main over program.ts,
which now registers the audit, related, saved-view, extensions and help
commands and the --utc option from the kernel branch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
packages/tui on OpenTUI 0.8.1 (React) and Bun, built on the kernel: a store
that holds all state and dispatches every key (lock, overlay, F-keys,
focused pane, fallbacks) so handlers never read stale closures; screens as
route entries that keep their own state (grid cursor, filter, selection) so
Esc returns to the same row; themes as token sets with high-contrast as the
default; sign-in (profiles, MOCK sessions, OAuth device flow), home, app
home with widgets, table grid over the kernel ResultCursor, record view with
child lists and next/previous, forms on FormSession with a narrowing picker,
the process wizard on runProcess, the two-mode filter dialog, view settings,
the F9 command center, palette, help, the : line with history, and the
screen lock.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brings in the extensions, related-records and query-gaps streams. The parity
matrix is regenerated from the merged STATUS table; process downloads already
use the shared safeFileName/saveDownload helpers from the base.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
qqq login --browser signs in with the authorization code and S256 PKCE on a
loopback redirect (RFC 8252): a one-shot listener on 127.0.0.1 takes the
callback, the state is checked, and the code is exchanged at the provider with
the verifier (no nonce, as UI-API-CLIENTS.md section 7 asks). --port,
--redirect-host and --no-open cover exact registered URIs and headless hosts.
A denied request, a forged state or a refused exchange exits 3 and saves
nothing; the new token is checked against the server before it is kept.
qqq logout --provider-logout revokes the refresh token (RFC 7009) and opens
the provider end-session URL with the ID token, which refreshes now keep. A
refresh the provider refuses becomes UNAUTHENTICATED (exit 3, sign in again).
qqq-dev-idp gains the authorize endpoint (Approve/Deny page or auto-approve,
loopback redirects and S256 only), the authorization_code grant, ID tokens,
revocation and end-session. The device flow was run end to end for the first
time against a sample on OAUTH2 (8078) and worked unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Conflicts resolved by keeping both sides: table get keeps the record view
(sections, links, reveal, help) and gains --join and --variant; create and
update keep uploads and parents and send the variant; filters keep the
DATE_TIME to UTC conversion and accept joined names; export keeps safe
file names. My server findings are renumbered 18 and 19 after the merged
13 to 17. Joined DATE_TIME values are now shown in the local zone too.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Conflicts: core index exports (records, render/files and extensions all kept), CLI context
(imports from both sides; UnsafeFileNameError exits 2 and ExtensionNotInstalledError exits 5),
server-findings (the presence finding is now #18, after related records' #16 and #17). The parity
matrix regenerates unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Opening a view runs its filter, sort, columns and page size; --where and
--search are ANDed on, --order and --fields replace. Duplicate names are
refused before any change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brings in the DATE_TIME work (local-zone display and input, --utc, process input exit codes).
The record view now formats DATE_TIME through displayDateTime (the viewer's zone, --utc for
the raw instant, server-zoned text kept), and so do the child lists of table get
--associations and table children; create/update keep the base's conversion through the
form session, including the --parent and --upload paths. process.ts keeps the base's value
formatting with the unified safeFileName/saveDownload. Exports, TRY-IT sections and parity
STATUS merged; this branch's findings renumbered to 16 and 17; matrix regenerated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Conflicts: core index exports (both kept), CLI context imports (both), main.ts registrations
(extensions and help), server-findings (the presence finding renumbered to #16 after #11-#15).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- TRY-IT: "Extensions (qbits)": what happens when an extension is installed, absent, too old or
unknown; the noun table (esb, collab and the generated nouns); running qbit-collaboration's demo
server on 8098 beside the sample server.
- packages/core/src/extensions/README.md: registry.has()/require() and joinPresence for the TUI.
- docs/extensions.md: every merged extension document with its version and repository commit.
- server-findings #11: an emptied presence scope is sent without `viewers`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The process screen had its own safeDownloadName and the CLI process command its own wx
writer, next to render/files' safeFileName and saveDownload. Keep one: safeFileName (last
path segment of either separator; empty, dot, dot-dot and control characters refused) and
saveDownload, which now takes the process writer's friendlier rule for default names (flag wx,
then name-1.ext, name-2.ext when taken) and returns the path written; an explicit --out still
replaces the file. process run DOWNLOAD_FORM, table download, table export and report run all
use it; the duplicates and their test move into files.test.ts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
forms/datetime converts typed wall-clock times (2026-10-31T14:30) to UTC in
the TZ zone with Intl offsets: a time in a spring-forward gap moves forward,
a repeated fall-back time means its first occurrence, and an unchanged
wall-clock time keeps the original instant with its precision. Instants are
shown as 2026-10-31 14:30:00 CDT; display values the server formats in a
declared zone are kept; --utc shows raw instants.
The form session converts DATE_TIME on set (table create, update, copy and
process values), DATE_TIME validation now requires a date and time, --where
and --group criteria on DATE_TIME fields are converted (a date alone is local
midnight), and table get/query and process screens format in the zone.
Tests cover Chicago, London, Sydney and Kolkata DST edges; verified live on
fieldLab (create, get, --utc, TZ=Asia/Tokyo, filters, update across the gap,
fieldLab.bulkEdit values).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- `qqq <extension> ...` for every extension the client document carries (collab for
collaboration, alias collaboration): each operation is a generated subcommand (method plus the
literal path segments, `get-records-comments <table> <pk>`), with path parameters as arguments
or --path, plus --param, --body-json, --form and --out.
- First-class `qqq esb overview|table|process|dead-letters|messages` and `qqq collab presence
[--watch|--observe] | comments list|add | favorites list|add|remove | recents | follow
[--status] | unfollow | my-work | people`.
- `qqq extensions`: known and installed extensions, versions, minimums, operations, channels;
extensions the server lists but the client does not know are listed, never dropped.
- `qqq api <op>` of an absent extension stops before the request; `qqq api --list` has an
extension column and `--extension <name|core>`. `qqq whoami` describes extensions through the
registry (unknown ones marked).
- ExtensionNotInstalledError maps to exit 5 with the installed list. QQQ_EXTENSION_CHECK=off
treats every known extension as installed, for Prism mocks.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Live against the collaboration qbit's dev server (QQQ 4.1.0-RC.15, collaboration 1.1.0): when the
last person leaves, presence.changed carries {"table": "person"} with no `viewers` at all, so the
handler treated it as no snapshot and kept showing the last viewer. A payload with `table` is now
the snapshot, viewers defaulting to none. The too-old error reads "this client needs extension X
1.1.0 or later".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Conflicts resolved: core index keeps every export; table get keeps --key (Go To) and gains
the record view, --associations, --links, --follow, --reveal, --help-content; table export
keeps the new formats, --search and stdout with the kernel's safe default name; README module
table and TRY-IT keep both; server findings keep 11-14 from the base and renumber this
branch's to 15 and 16; parity STATUS merged row by row and the matrix regenerated. The record
view's help helpers are renamed helpEntryText and tableViewHelp, since the base's help and
process screen modules export helpText and tableHelp.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Without a terminal, a validation review step now stops with a usage error
(exit 2) unless --validate or --skip-validation is given, instead of letting
the server default. A step ERROR that is user-facing, or that reports missing
required input (placeAddressPick without values), exits 6 rather than 1.
Finding 15 records the missing userFacingError.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A table that uses variants without --variant asks on a terminal and
otherwise exits 2 without querying. process run sends the variant on init,
every step and status. Writes send it too, with a note that UI API 1.8.0
ignores it there (server finding #13).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Joins follow the web UI: only the joins a query uses (named, or referenced
by columns, criteria or sorts), LEFT with the join name on one-hop paths,
and a distinct count for many-side joins. Association joins cover tables
that expose none. Variants list from the v1 route and resolve by id, name
or type:id.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
TRY-IT gains the related-record and record-view commands as verified against the sample.
Parity: REL-001..005 and 009, REC-028 pass live; REL-006..008, REC-002, REC-029..041 are
partial because the sample declares no such metadata (kernel unit tests cover them).
Findings: downloadRecordFieldFile 500s on a BLOB without adornments, and the sample lacks the
adornment, help and composite-association metadata needed to check those rows live.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
table download, table export and report run now name a default output file through
safeFileName and write it exclusively (an existing file is an error naming --out); an
explicit --out may still overwrite. An unsafe name from the server exits 2.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A file name from the server (a record's file, a content disposition, a process download)
was used as a path by table download, so a name like ../../etc/passwd could write outside
the working directory. safeFileName keeps only the final path component (either slash
style) and refuses empty, dot, dot-dot and control-character names; saveDownload creates a
default-named file exclusively, so only a path the user named is ever overwritten. Kernel
code so every download path (table download, export, reports, process DOWNLOAD_FORM) uses
the same rule.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
DOWNLOAD_FORM names come from the server. safeDownloadName keeps the last path
segment only (either separator), strips control characters and refuses empty,
. and ..; qqq process run writes with flag wx and takes name-1.ext, name-2.ext
when the name is taken. Tested with traversal names.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Keeps both export sets in the kernel index, renumbers the process findings to
13 and 14 after the query-gaps findings, and regenerates the parity matrix from
the merged STATUS table.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
table get now shows the record by section through the kernel's record view: hidden
sections and fields never appear, empty values show --, adornments render as text (record
links name their table and key, masked values with --reveal, files with the download
command), --help-content adds table, section and field help, --links lists the related
records and --follow opens one. --associations prints every association as tables, all
levels (person > pets > notes), honouring the table metadata, with unreadable child tables
marked unavailable.
New: table children pages one child list with its count (or summarises every association);
table create --parent table=pk [--association] links a new child to its parent; create and
update take --upload field=path as multipart/form-data; table download saves a stored file
(FILE_DOWNLOAD route or BLOB value).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Child-list columns followed the server's field map order (Modify Date before Name); order
them as the record view does (key, then sections by tier, never fields only in hidden
sections). fetchChildren with limit 0 now only counts, for association summaries.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
session/local-state.ts keeps client state that is not configuration:
persistent capped command histories under ~/.local/state/qqq (never a line
that carried a password, PIN or token), recently viewed records per server
in the config file's ui section, and the screen-lock PIN as a salted scrypt
hash (never the PIN) with an idle timeout and a growing retry delay.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The kernel gains a screen model (process/screen): each step's components in
declared order become neutral blocks both heads render - HELP_TEXT and HTML as
text, VIEW_FORM pairs on every step, RECORD_LIST paged through GET .../records,
the validation review with its preview and summary, PROCESS_SUMMARY_RESULTS
lines, DOWNLOAD_FORM references (GET /download), WIDGET through the widget
render model, and step help content.
The runner presents each screen, asks the provider on every input step
(validation review included), restarts at backStep with isStepBack=true when
the provider returns BACK, cancels on an abort signal with POST .../cancel,
and checks min/max input records before init. A step whose components are
only a VIEW_FORM no longer counts its formFields as inputs.
qqq process run prints intermediate screens to stderr and the last as its
output, asks validate-or-skip and Submit? on a terminal, saves downloads to
--out-dir, and turns Ctrl-C into a server cancel (exit 130). Verified live on
sleepInteractive, greetInteractive, clonePeople, simpleThrow, placeAddressPick
and person.bulkEdit's review screen; findings 11 and 12 recorded.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
extensions/registry: every extension the client document carries (name, path prefix, document
version, documented minimum version, realtime channels it publishes on, its operations), joined
with the server's GET /version `extensions`. `has`, `version`, `require` (typed
ExtensionNotInstalledError: "extension X is not installed on this server (installed: esb 1.1.0)")
and `requireOperation`. Extensions the server lists but the client does not know are kept and
described, never dropped. GET /version is cached next to the metadata (same TTL, offline copy).
extensions/collaboration: presence as the qbit specifies it (POST lease every 20 s, DELETE on
leave, GET snapshot, presence.changed on record:{t}:{pk} / table:{t} after subscribing on hello,
polling when there is no stream, reconnect with backoff), with `since` tracked per person by the
client; a no-op handle when the extension is absent. Favorites, recents, comments, follow, people
and my-work calls throw the typed error before any request when it is absent.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Export checks the format and field names before the request (the server
answers 500 for both), takes the format from --out, writes to stdout with
--out -, and can drop the header row.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Kernel models the TUI can reuse: the web UI's quick search (CONTAINS over
shown text fields, ORed, ANDed onto the filter), Go To lookups by the
primary key or Material gotoFieldNames, base-copy values, app home sections
and report listings from metadata, and helpContents as text. Table variant
helpers come along because lookups take an optional variant.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Related records and the record view belong in the kernel so the TUI reuses them. The
association model binds children to parents through the join (composite joins too),
builds the child-list filter, walks a record read with includeAssociations into labelled
child lists (person > pets > notes), marks unreadable child tables unavailable, and
computes the link values for creating a child under a parent. The record view drops
hidden sections and fields and renders each adornment as text (LINK, CHIP, REVEAL,
RENDER_HTML, CODE_EDITOR, ERROR, FILE_DOWNLOAD, WIDGET, TOOLTIP), following the Next UI,
with view-screen help selection by role.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
main.ts becomes a thin entry over program.ts, which exports createProgram()
and runCli(argv, { stdout, stderr }): one command line run in-process with
its output captured, prompts off. The TUI's : command line uses it, so the
CLI grammar is the TUI's command language and both heads share one
dispatcher. Context output goes through an io object that runCli points at
the writers; the few commands that write to process.stdout directly are
captured for the duration of the run.
qqq ui [route...] runs packages/tui/src/main.tsx with Bun, passing the
profile, server, theme, --ascii, --linear and --mouse options and an
optional route in the CLI grammar.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The client document is now the sample server's live document (core plus esb, UI API 1.8.0) merged
with the extension documents of every qbit checked out beside this repository: health (qqq), alerts,
assistant, collaboration, data-analysis, db-performance, edi, qa-capture, quick-search,
release-notes, secured-data and sql-workbench. 202 operations, 136 of them from extensions.
- scripts/merge-spec.mjs records each extension's path prefix and operationIds in
info.x-qqq-extensions, and accepts an extension the core input already carries when its
operations are identical (the live document's esb), instead of failing on the duplicate.
- scripts/merge-extensions.mjs (npm run spec:merge-extensions) finds the documents, merges them and
writes openapi/extension-sources.json with each document's version, branch and commit.
- scripts/gen-operations.mjs adds `extension` to OperationInfo (from x-qqq-extensions, else the
/qbit/{name} or established prefix) and exports DOCUMENT_EXTENSIONS.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Additive kernel helpers the TUI head needs and the CLI can adopt:
- session/login.ts: the profile's stored sign-in and a fetch that sends a
server session as the sessionUUID cookie and OAuth2 tokens as bearer
(refreshed when expiring), same keyring account as the CLI.
- session/profiles.ts: a ui section (theme, per-screen view settings) in
~/.config/qqq/config.json, preserved by readConfig, with read/write helpers.
- filter/complete.ts: what can be typed at the cursor in the filter grammar
(fields, operators valid for the type, values and expressions, AND/OR and
parentheses), operatorsForField, error location and a filter description.
- records/result-cursor.ts: a position in a query result with a page cache,
next/previous across pages, and delete bookkeeping.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The runner sends a file as a multipart part with the first step whose form has that BLOB
field, and treats a pending file as a provided value. Verified against the sample's
person.bulkInsert upload step once the sample accepts uploads (finding 10).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The orb's node_install_dependencies runs npm run prepublishOnly (now the workspace build) and
node_run_tests runs npm test then npm run lint. lint checks license headers and that no
package imports another package's src. Also records finding 10: the sample app has no
uploaded-file archive table, so bulk load cannot be exercised against it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/parity/feature-matrix.md keeps the 401 Next UI row ids and adds a CLI and a TUI column
(pass, todo, partial, n/a with reasons), computed by scripts/parity-matrix.py from the copied
Next matrix (docs/parity/next-feature-matrix.md) and the status table in the script. First fill
of the CLI column after the M1 work so far.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
session/profiles.ts keeps named profiles (server, note, default) in ~/.config/qqq/config.json
and resolves --profile, QQQ_PROFILE, --server, QQQ_SERVER or the default. session/keyring.ts
stores secrets without native modules: macOS Keychain through security, Linux Secret Service
through secret-tool, Windows DPAPI through PowerShell, and a 0600 file otherwise or with
QQQ_KEYRING=file. The CLI's session store moves onto them, imports the 0.1 credentials file
once, and adds --profile plus qqq config show and config profile list|add|use|remove.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
metadata/cache.ts keeps GET /metaData and table definitions on disk per server (five-minute
TTL, offline reads, refresh and clear). Listings and table commands read through it, so a
cached query round-trips in tens of milliseconds. qqq completion bash|zsh|fish|pwsh prints a
small script that calls the hidden qqq __complete, which answers from the command tree and
the cache: nouns, verbs, options, tables, processes, widgets, reports, fields after --where,
--order, --fields and --set, output formats, and operation ids for qqq api.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
widgets/render-model.ts turns any WidgetData shape into neutral blocks (heading, stat, table,
series, pairs, steps, alert, link, rule, children) and prints them for a terminal: one-dataset
charts as bars, several datasets as a labels-by-datasets table, html through its text sibling or
stripped, composites nested, permission refusals as a warning. qqq widget get renders or
exports --csv; report run saves a report; prefs list/get/set/action and help-search wrap the
preferences and help operations. docs/server-findings.md lists the candidate qqq issues found
so far.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The kernel gains forms/: field dependencies (possibleValueSourceDependsOn or ${input.x} in a
filter), DISABLE_UNTIL_SET semantics, dependents cleared transitively when a dependency
changes, possible-value search with the dependency values, resolution of typed text by id,
exact label or unique partial (with a client-side fallback when the source matches
case-sensitively), server-mirroring validation (required, type, length, range, pattern),
behaviors, form adjusters (onLoad and onChange, 404 means none), and a FormSession that
both heads drive. process/runner.ts runs init and every step through a provider callback,
polling async jobs. The CLI's table create/update and process run go through the session:
--set and -v accept labels, -i prompts on a terminal with choices that narrow after each
answer, and invalid values fail with exit 6 before any request.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
qqq <noun> <verb> for table (list, describe, query, count, get, create, update, delete, export),
process (list, describe, run) and job (list, get, logs, cancel, retry), with the 0.1 flat
commands kept as hidden aliases. Global --output json|table|text|csv|yaml, --json, --query
(JMESPath), --fields, --yes and --trace work before or after the subcommand. Filters take
--where, --group (AND/OR with parentheses), --or, --order, --filter-json, --print-filter and
--count; queries page with --limit, --skip and --all. qqq api <operationId> calls any
operation from the generated index (--path, --param, --body-json, --form, --bytes, --out).
Exit codes: 2 usage and bad filters, 3 auth, 4 unreachable or rate limited, 5 not found,
6 validation, 70 server error. The realtime stream can carry a session cookie for tail.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
packages/core is the client kernel both heads build on; no terminal code. This first cut has
render/output (json, table, text, csv, yaml, JMESPath --query, tty-aware default) and
filter/grammar: criteria with shorthand operators, quoted values, AND/OR groups with
parentheses, and the backend's expression values (now(), now(-7d), thisPeriod(month),
startOf(quarter,-1), me(), ${variable.x}), parsed and printed back identically.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The client gains getAuthenticationMetaData, createSession (reads the HttpOnly sessionUUID
cookie) and a sessionCookie option; UI_API_VERSION is 1.8.0. qqq login branches on the
server's authentication type and stores a server session for MOCK and FULLY_ANONYMOUS; the
device flow stays for OAUTH2 and AUTH_0. The session goes as a cookie because a bearer
session cannot continue asynchronous process steps on the 4.1.0-SNAPSHOT sample server.
scripts/dev/qqq.sh runs the CLI from source so every commit is testable without a build.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Taken from a running QQQ 4.1.0-SNAPSHOT (develop 6ac64fc66) sample server, so it carries core
plus the esb extension only; the other extension documents are merged back from their qbit
repositories when those are next built. The CLI and conformance packages compile unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
So the person testing always knows which commit the shell is, and whether it carries
uncommitted changes. TRY-IT.md explains that there is no install step.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
James wants the feel of a great old TUI: function-key bar, Page Up/Down everywhere, a
real-time footer. The shell adds a title bar with breadcrumbs, the app tree, a server-paged
table grid, record view by section tier, the process screen, help/go-to/menu/confirm/prompt
overlays, a ':' command line, and a status line with ping, stream state, session, API
version, jobs and clock. Per-screen error boundary so a throwing screen cannot take the
terminal down. TRY-IT.md documents the keymap; SPIKE-RESULTS.md records what it taught us.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Throwaway OpenTUI (React) spike on Bun: 10k x 40 grid, every-field-type form, and a real
UI API process run with an async job against the qqq sample server. Records the go
decision, the measured latencies, binary sizes, and two server findings (bearer session
cannot continue an async process step under MOCK auth; url-encoded process values are
silently ignored). Adds scripts/dev launchers so the client can be tried after every step.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-31 15:48:34 -05:00
27 changed files with 138 additions and 1179 deletions
| `api <operationId>` | any UI API operation by id |
| `extensions`, `esb`, `collab`, `health`, `alerts`, ... | one noun per extension, run when the server has it installed |
| `assistant ask`, `assistant config` | ask the in-app assistant (qbit-assistant) a question about your data: the answer streams block by block, rendered (tables as sub-tables, links as `label (qqq table get …)` or `label [n]` with the command under Links, never a URL), then its numbers (`--table`/`--record` send the page as context, `-c` continues a conversation, `--json` the whole answer); `config` says whether it answers and with which model, or why not |
| `assistant ask`, `assistant config` | ask the in-app assistant (qbit-assistant) a question about your data: the answer streams, then its numbers and the records and tables it refers to (`--table`/`--record` send the page as context, `-c` continues a conversation, `--json` the whole answer); `config` says whether it answers and with which model, or why not |
| `print`, `printer` | print records, table lists and text through the printing extension, follow print jobs; printers and print agents |
| `history` | everything you did with the CLI and the TUI, like a shell history (see below) |
"text":"3 services are not fully healthy: one degraded (email) and two with unknown status. None are down.\n\nUnhealthy services\n\n| Service | Status | Since | Checks up | Degraded | Unknown |\n| --- | --- | --- | ---: | ---: | ---: |\n| [email](/app/health_component?filter=%7B%22criteria%22%3A%5B%7B%22fieldName%22%3A%22id%22%2C%22operator%22%3A%22EQUALS%22%2C%22values%22%3A%5B4%5D%7D%5D%2C%22booleanOperator%22%3A%22AND%22%7D) | DEGRADED | 2026-11-01 11:25 UTC | 0 | 1 | 0 |\n| [db_performance](/app/health_component?filter=%7B%22criteria%22%3A%5B%7B%22fieldName%22%3A%22id%22%2C%22operator%22%3A%22EQUALS%22%2C%22values%22%3A%5B3%5D%7D%5D%2C%22booleanOperator%22%3A%22AND%22%7D) | UNKNOWN | 2026-11-01 17:25 UTC | 2 | 0 | 1 |\n| [esb](/app/health_component?filter=%7B%22criteria%22%3A%5B%7B%22fieldName%22%3A%22id%22%2C%22operator%22%3A%22EQUALS%22%2C%22values%22%3A%5B10%5D%7D%5D%2C%22booleanOperator%22%3A%22AND%22%7D) | UNKNOWN | 2026-11-01 11:23 UTC | 1 | 0 | 1 |\n\n- [Open unhealthy components](/app/health_component?filter=%7B%22criteria%22%3A%5B%7B%22fieldName%22%3A%22status%22%2C%22operator%22%3A%22NOT_EQUALS%22%2C%22values%22%3A%5B%22UP%22%5D%7D%5D%2C%22booleanOperator%22%3A%22AND%22%7D)\nThree services aren't fully healthy right now:\n\n- **email** is degraded. Its only check is degraded.\n- **db_performance** is unknown. One of its three checks has no status.\n- **esb** is unknown. One of its two checks has no status.\n\nNo service is down.",
shortcuts:'single-key shortcuts on or off',lock:'lock the screen',pin:'set or remove the lock PIN',idle:'screen saver after idle minutes',refresh:'reload this screen',
filter:'filter the grid (grammar)',export:'export the grid or result',jobs:'your jobs',printjobs:'print jobs',print:'print what you see (or :print <printer>)',profile:'switch profile',servers:'saved servers (Ctrl+O)',
history:'everything you did (CLI and TUI)',server:'connect to a server URL',signout:'sign out',connection:'connection details',open:'open a table',record:'open a record by key',app:'open an app',
sql:'SQL Runner (statement to run, open <notebook id>)',ask:'show or hide the Assistant panel (F12, Ctrl+G); with a question, asks it about this screen',saver:'the screen saver',doctor:'check the terminal',views:'saved views of this table',share:'share this record',helpkeys:'help-authoring mode',
sql:'SQL Runner (statement to run, open <notebook id>)',ask:'the Assistant panel (Ctrl+G); with a question, asks it about this screen',saver:'the screen saver',doctor:'check the terminal',views:'saved views of this table',share:'share this record',helpkeys:'help-authoring mode',
}
/** CLI commands the `:` line does not offer (they make no sense inside the TUI). */
@@ -426,10 +426,7 @@ export class Commands implements CommandsLike {
@@ -731,7 +728,7 @@ export class Commands implements CommandsLike {
out.push({section:'Work',label:'My work',disabled: s.extensions.has('collaboration')?undefined:'needs the collaboration extension'})
out.push({section:'Work',label:'History',command:'history',detail:'everything you did, CLI and TUI',run:()=>openHistory(s)})
out.push(...sqlMenuEntries(s))
out.push({id:'work:assistant',section:'Work',label:'Assistant',key:'F12',command:'ask <question>',detail:'F12 assistant (Ctrl+G too): ask about your data, with this screen as context',disabled: s.extensions.has('assistant')?undefined:'needs the assistant extension (qbit-assistant)',run:()=>s.openAssistant()})
out.push({id:'work:assistant',section:'Work',label:'Assistant',key:'Ctrl+G',command:'ask <question>',detail:'ask about your data, with this screen as context',disabled: s.extensions.has('assistant')?undefined:'needs the assistant extension (qbit-assistant)',run:()=>s.openAssistant()})
for(consteofs.extensions.all())out.push({section:'Work',label:`Extension: ${e.name}${e.version}`,detail: e.description,command:`api --list`,run:()=>s.message(`${e.name}${e.version}\n${e.description??''}\nPath: ${e.pathPrefix??''}\nIts panels are not drawn by this TUI yet; its operations are reachable with :api.`,'info','Extension')})
// Connection
out.push({section:'Connection',label:`Server ${s.status.server}`,detail:`UI API ${s.status.apiVersion}`,command:'connection',run:()=>this.connection()})
['Ctrl+P PrtSc','print what you see: a record sheet, a table list (all rows or this page), or the screen; Ctrl+P always works, Print Screen only where the terminal passes it on'],
['Ctrl+O','servers: switch, add, edit'],
['F12 Ctrl+G','F12 assistant: show or hide the Assistant panel (this screen is the context); :ask <question>'],
['Ctrl+G','Assistant panel: ask about your data (this screen is the context); :ask <question>'],
[':','command line; Tab completes, ↑↓ history, Ctrl+R searches it; :history lists all you did'],
console.log(`docs/packaging/homebrew-tap/Formula/qqq.rb (${out.formulaCount} sums) and docs/packaging/qqq.json now name ${version} with the sums from ${sumsPath}`)
}catch(error){
console.error(`update-packaging: ${error.message}; nothing was written`)
process.exit(1)
}
}
// Run only as a script, not when the tests import it (realpath: argv[1] may go through a symlink such as macOS /var).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.