feat(cli): full-screen terminal app (#1544, row 55)

Bare `mosaic` on a terminal opens the app. It has Inbox, Tasks, Agents
and Trail screens, help, and a decide overlay, and it uses the alternate
screen, raw mode, resize and SIGTERM, and restores the terminal on exit.
draw(state) in screens.mjs is pure. cli.mjs exports resolveDecision,
which `mosaic decide` and the app share, so both write the same lines
and ask the same question. A decision resolves on `y` only while
decideShown() says the whole decide box is on screen. The app reads that
at the key. A box that doesn't fit is replaced by "Too small to show this
decision", and any key answers No. scripts/mosaic sends no arguments to
packages/cli (a decided scope addition), so the queue dispatch test
follows. The cli README and docs/TOOLS.md describe the app.

Darkwing built it (agents/darkwing/work/queue-55), in three rounds.
Round 3 was approved by Dewey (27308) and Filbert (27309) on manifest
59f1b9b4, 9 paths. Their non-blocking notes go to row 69 (#1562): a
narrow-terminal test for the decideShown MIN_COLS guard, a way out from
the hidden box, a U+18D08 width test, a cut box keeping Esc back, and
the README row count stated as an example.

Sage's gate on 3634d06e, sequential and teed: node suites cli 110 and
queue 150, 0 failed; test-auth 15, test-conductor 17, test-config 24,
test-discord 66, test-extension-package 18, test-foundation 44,
test-queue 27, test-release 14, test-task 98, all 0 failed (Docker on).
122b1223..3634d06e touches none of the 9 paths.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-11 16:06:11 -05:00
co-authored by Claude Opus 5.5
parent 1e71d74670
commit 79d9fc2d56
9 changed files with 1932 additions and 16 deletions
+74 -2
View File
@@ -4,12 +4,16 @@ Jason's front door to the bus (slice 1 row S4, #1521): `mosaic inbox`,
`decide`, `tasks`, `agents` and `trail`, and the bus host `mosaic bus
start|stop|status` with its Discord notifier (row S4, #1521); the session
launcher in the host and `mosaic talk`, `stop` and `launches` (row S6,
#1523). Brief: `docs/plans/2026-10-04_slice-1.md`, rows S4 and S6. Rulings:
lead decisions 70 and 77 in `docs/plans/2026-09-26_lead-decisions.md`.
#1523); and `mosaic` with no argument, the full-screen app (queue row 55,
#1544). Briefs: `docs/plans/2026-10-04_slice-1.md`, rows S4 and S6, and
`docs/plans/2026-10-10_design-implementation.md`, "Full-screen terminal
app". Rulings: lead decisions 70 and 77 in
`docs/plans/2026-09-26_lead-decisions.md`.
Every command reads through the broker. None opens the SQLite file.
```sh
scripts/mosaic # the app, on a terminal
scripts/mosaic inbox
scripts/mosaic decide 3f2a9c1e yes --note "ship it"
scripts/mosaic trail 3f2a9c1e-… # then: scripts/mosaic trail vikunja:1/7
@@ -70,6 +74,74 @@ mosaic launches list [--json]
`launches list` does not, because the file is readable to the same user
anyway.
## The app
```
mosaic
```
With no argument, on a terminal, `mosaic` opens a full-screen app over the
same broker reads as the verbs: Inbox, Tasks, Agents, Trail and Help.
`src/app.mjs` handles keys and reads; `src/screens.mjs` draws the frames
and nothing else.
- In an agent run it exits 3 with the same refusal as the verbs, before
anything else. Without a terminal on both stdin and stdout it exits 4
and prints the seat usage (`mosaic launch`, `mosaic seat task`) and the
verbs above. A script never gets the app.
- The business is the one of the bus host on this data root. With no
host the app says so and `:business <id>` picks one.
- Keys: `c i t a r` switch views (Talk, Inbox, Tasks, Agents, Trail). `↑↓`
or `j k` move, PgUp and PgDn page, Enter opens the selection whole, `v`
opens its trail, `:` the command line, `?` help, Esc closes a box, `q`
or Ctrl-C quits. On the command line, Esc or Ctrl-C closes it and the
arrows type nothing. Commands: `:decide <decision> <option>`, `:trail
<task|decision>`, `:business <id>`, `:q`.
- Deciding: in the Inbox, digit `n` picks the selected decision's `n`th
option and runs `resolveDecision` in `src/cli.mjs`, the code `mosaic
decide` runs. The box shows the same lines and the same `[y/N]`
question. `y` or `Y` resolves; any other key answers No, and so does
quitting. Keys that arrive within 200 ms of the question being drawn
are type-ahead and are dropped: a paste such as `1 yes`, or keys typed
while a read held the screen, never answer it. After `outcome unknown` the app shows the CLI's sentence and
offers the trail (`v`) or a re-read (`r`). No key sends the choice
again.
- Reads happen on entering a view, after a decision, and every 30 s while
no box is open. A failed read keeps the last list on screen and marks
it stale. A refused read (exit 3 codes) says refused. Broker text goes
through `visible()` from `packages/conversation/src/terminal.mjs`, so
control characters and bidi overrides show as `^[` and `<U+202E>`.
- Talk (`c`) points to `mosaic talk` for now. The conversation pane and
`stop` come in a later row.
- 16 ANSI colours. Under `NO_COLOR` the only escape codes in a frame are
reverse video on the selected row (the design's tokens keep selection
visible without colour). Frames fit 80x24; the layout is designed at
120x36, and under 100 columns the nav shows keys only and the Inbox
drops RAISED BY. Below 40x10 the frame only says the terminal is too
small. If the decide question is open then, the frame says so, and any
key answers it No, `y` included. Widths count terminal cells: East
Asian Width W and F characters and emoji 2, a one-cell emoji base with
U+FE0F 2. ZWJ sequences and flags count more than they draw, so such a
row can come out short, never too wide. Wrapping breaks between
graphemes, so U+FE0F and combining marks stay with their base.
- A decide box taller than the screen cuts the end of the question and
nothing else: the decision line, the options, the recommendation, the
action and target, what the choice authorizes, the task, your choice
and the `[y/N]` question stay. The cut line points to `mosaic inbox
--json`. When even those lines don't fit, the box says only "Too small
to show this decision. Any key answers No." `y` resolves only while the
frame shows the whole box apart from the cut, and the app checks the
size at each key, so a terminal shrunk after the question appeared
answers No. A gated decision with a task needs 20 rows at 80 columns
(21 with a cut question), and 29 rows at 40 columns.
- The transport is the verbs' `spawnSync` child, so the screen does not
redraw while a read is in flight (up to its 30 s timeout).
- On quit, end of input, SIGTERM, SIGINT, SIGHUP or an error (one in a
resize redraw included) the app restores the screen, the cursor and
cooked mode. After anything but an error it then prints
`(app closed with q; N decisions resolved this session)`; an error
goes to stderr instead.
## Bus host
```