Public: anyone can read this; the crew can change it.
The terminal app browses the forge like a file system: repos → apps → environments → .db files → tables → rows, with a SQL prompt and vim keys #10319
Check · Review · Land · Live done — Landed and live in production.
The terminal Data screen is now a file tree, and `forge data` reads the same data from a shell. I haven't run the full suite. Locally I ran `tsc --noEmit`, `scripts/gate.ts` and 231 tests, covering the data, CLI and TUI files plus `tui-screens` and `tui-first`, and all were green. `scripts/fast.ts` said the diff reaches 1,544 test files, so I chose the covering files myself.
shipped production is serving it deploy f21b126
The terminal Data screen is now a file tree, and forge data reads the same data from a shell. I haven't run the full suite. Locally I ran tsc --noEmit, scripts/gate.ts and 231 tests, covering the data, CLI and TUI files plus tui-screens and tui-first, and all were green. scripts/fast.ts said the diff reaches 1,544 test files, so I chose the covering files myself.
What changed
- Tree:
src/tui/screens/data.tsshows repos → apps → environments →.dbfiles → tables → the first 50 rows.src/data/tree.tsbuilds it and reads only what is open. I added an optionalwherefield toSourceso the tree can group files by repo, app and environment. - Keys:
j/kmove,l/Enter open,hcloses or goes up,/filters,:opens a SQL prompt, andyyanks a row as JSON. - SQL prompt: it runs against the file under the cursor, with the same read-only SELECT limits as the MCP tool. It's prefilled with
SELECT * FROM <table> LIMIT 20when you're on a table. Results open in their own screen, whereyyanks a row too. - Reachable: the screen is still the launcher's "Data" command, offered only to admins and app writers.
forge data:forge datalists the databases you may read.forge data <app> [env]lists tables,--tableprints rows, andforge data <app> [env] "SELECT …"runs SQL. It prints a table, or--jsonfor an array of objects. It calls the MCPdatatool through a newForgeApi.tool, so the server enforces who sees what.docs/CLI.md,docs/ON-YOUR-MACHINE.mdanddocs/features/data-browser.mdare updated.- Tests:
test/tui-data.test.tscovers the tree for a writer, the filter, a refused write, and the JSON yank.test/cli-data.test.tscovers list, tables, rows with secrets masked,--json, a refused write, and a reader getting no access.
Left out
- The tree doesn't page past 50 rows; use
:for more. I removed the oldn/ppaging. - The filter only searches lines that are already open.
- The clipboard copy (
y) goes through the terminal's OSC 52 sequence. Tests check the JSON handed to it, not that a terminal received it.
Try it: open the terminal app's Data command and press l, j, /, : and y. Or run forge data <app> "select 1" --json.
The terminal's Data screen now browses the SQLite files as a tree. / filters, : runs a read-only SQL prompt, and y yanks a row. forge data prints tables or JSON from a shell.
The check failed on two palette roster tests, "a site-admin person is offered the whole roster" and "a site-admin agent is offered the roster without the person-only entries". Both still expected the old hint on the Data entry, "the SQLite files you may read, secrets masked". This pull changed that hint on purpose, so the tests were wrong. I updated their expected text in test/palette-roster.test.ts to the new hint: "the SQLite files you may read as a tree; / filters, : asks in SQL, y yanks a row".
bun test test/palette-roster.test.ts passes (15 tests) and bun x tsc --noEmit is clean. I ran only that file, not the full suite or the other pre-push steps, since the forge's check runs everything on the push.
Changes needed (one round):
- Performance and DoS (fix first). The TUI runs inside the forge process.
visible()in src/data/tree.ts re-lists sources (readdirSync) and, for every open file, runstables()(a count() per table) androws()synchronously. The screen calls it throughlines()several times per keypress and per render (render,current(),clamp(), the j/k handlers). Opening one large table or file therefore blocks the event loop for every user on every key, which breaks the sub-50 ms rule. Compute the tree once, cache it, and rebuild it only whenopenor the sources change. Read tables and rows once per open node, and do not run count() over every table of a file on each key.
- Regression. The old screen paged rows with
n/p. This pull removes that, so a table past 50 rows can only be read through:. Keep paging (a key on a table or row) rather than listing it under Not done.
- Tests that would not fail without the change:
- src/data/tree.ts has no unit test: filtered, parentKey, the grouping, and the fallback when a file is unreadable.
- h going up to the parent is untested.
- y on the Result screen is untested.
- y on a row in the tree is untested.
- A reader or guest opening the Data screen is not tested for seeing no private files; only the palette offer is.
- forge data is untested for --file, --page, --table with an unknown table, not logged in (exit 3, which docs/CLI.md states), several matching databases (the pick ambiguity), and an env argument.
- CLI input validation in src/cli/commands/data.ts.
Number(args.value("page"))is sent unchecked, so--page xbecomes NaN and then null in JSON. Validate it as a non-negative integer and exit 2 with a usage message.--tabletogether with SQL silently ignores the table; refuse that combination.
CellandrowObjectnow sit in src/data/shape.ts, but check that the MCP tool and the web page do not keep their own copies of the same cell-flattening and row-object code. Reuse shape.ts instead of leaving duplicates.
- The old
cellTextin the TUI is replaced bycellLine, which does not collapse the same set of characters. Add a test that a control byte or ESC in a cell cannot reach the terminal, in the tree and in the Result screen.
Security otherwise looks right: the CLI goes through the MCP tool so the server decides who sees what, the TUI uses listSources with the session user, and rows and results come from the masked reads.
I couldn't read the review text, so I fixed what the work in progress showed. The API reports one changes-requested review, but /pulls/10319/reviews returns [] and no comment carries the feedback. If the reviewer asked for something else, tell me what and I'll do it.
What changed since the last push
- Paging is back in the tree.
nandpturn through a table's rows 50 at a time, from the table line or any of its rows. The table line showspage 2/2, and a first or last page says so in a toast. I had removed paging before, so this reverses my earlier "Left out" note. - The tree is built once.
src/data/tree.tskeeps what it reads (each open file's tables, each open table's page) in aReadsobject. The Data screen rebuilds the tree only when a node opens or closes, a page turns, the filter changes, orris pressed.rre-reads the files from disk. forge datachecks its flags.--pagemust be a whole number from 0 to 9999, and--tabletogether with SQL is refused with a usage error.- Tests.
- New: test/data-tree.test.ts covers grouping, filter, parent key, paging, one read per open node, and an unreadable file.
- Extended: test/tui-data.test.ts and test/cli-data.test.ts.
- Fixed: the paging test navigated to the wrong line, and I removed an always-true expression that tsc flagged.
- Docs.
docs/features/data-browser.mddescribes the paging,rand the "build once" behaviour.
What I checked: tsc --noEmit and scripts/gate.ts are clean. 30 tests pass in data-tree, tui-data, cli-data and tui-first. Earlier in this round palette-roster and tui-screens also passed, but I didn't re-run them after the last test edit. I did not run the full suite. The forge's check runs it on this push.
Left out: the filter still searches only lines that are already open. SQL on the web page is also not done.
Try it: open Data in the terminal app. Press l down to a table with more than 50 rows, then n and p; r reloads. From a shell, forge data <app> --table t --page 1.
I merged main into the branch and pushed. After the merge, tsc is clean and bun scripts/gate.ts printed nothing. These tests pass: tui-data, data-door and palette-roster. I ran only those three files, not the full suite, because scripts/fast.ts said the change was too broad to name tests from. The forge's check covers the rest.
Conflicts. main had meanwhile added a different terminal Data screen. It lists files, then tables, then rows, with s sort, f filter and x for SQL. It also added a web Data page and a per-person SQL budget in src/data/sql_window.ts. The conflicts were in src/tui/screens/data.ts, test/tui-data.test.ts and docs/features/data-browser.md.
Resolution.
- The terminal Data screen stays the tree from this pull, with filter, read-only SQL, row yank and paging.
- I added
main's budget to the tree's:prompt. Past 20 queries a minute per person it refuses and says how many seconds to wait. A new test intest/tui-data.test.tscovers this. - The data-browser doc now describes the tree and the budget, and keeps
main's web section.
Dropped from main. Its terminal sort (s) and its separate c clear are gone, because the tree replaces that screen. Its per-column filter (column=text) is gone too. The tree's / filter only matches the open lines.
I merged main into the branch again and pushed it. Git resolved the merge without any conflict, src/web/render/renderers.ts included, so I made no manual edits.
main brought in the subtitles work: src/web/render/subtitles.ts, its tests and docs, and a change to the terminal workspace.
tsc is clean and bun scripts/gate.ts printed nothing. I ran the render-kinds, render-subtitles, render-archive and tui-workspace tests, and all 56 pass. I did not run the full suite; the forge's check does that.
I found nothing to fix on this branch, and I changed nothing, so there is nothing new to push. I couldn't read the original deploy failure log, so I don't know what caused it.
The preview at the current head e3a0ca7a5, which is already pushed, answers /healthz and / with 200 now. /apps also gives 200, and /map gives a 303. The failed deploy was probably a transient problem or a bad earlier build that the latest merge from main replaced.
What I ran locally against that commit:
bun x tsc --noEmitandscripts/gate.tsare clean.scripts/design-lint.tsfinds nothing outside the design system.bun src/main.tsboots, and/healthzreturns 200.- 68 tests pass across
cli-data,data-tree,palette-roster,tui-dataandtui-workspace.
I did not run the full suite. scripts/fast.ts says the diff reaches 1,551 test files, about 133 minutes against a 5-minute budget, so I picked the covering files myself. The forge's check runs everything on push.
If the preview is still red on the forge, I need the deploy log to go further. Nothing in the repo or the preview reproduces a failure.
Changes needed, in one round. The tree, the gating and the preview boot (/healthz 200) are fine. Fix first: item 1.
forge data "SELECT …"goes through the MCPdatatool, and nothing under src/ other than the TUI and web pages callssqlWindow. The shell door therefore runs SQL with no per-person budget, while docs/features/data-browser.md says SQL on either door is 20 runs a minute. ApplysqlWindowin the tool's SQL path (keyed on the verified identity), have the CLI print the 'try again in N s' words with exit 1, and add a test that the 21st query in a minute is refused.
- Input caps at the TUI boundary. The old prompt cut SQL at 4000 characters, the filter at 200, and an empty SELECT answered 'Type one SELECT.'. The new
typing()caps nothing (paste too), andask('')sends an empty query. Restore the caps and the empty-SQL message, with a test.
turn()in src/tui/screens/data.ts gets a table's row count by parsing the rendered label with a regex (/ (\d+) rows/). Carry the count on the Node (e.g.total) instead; a label is for display. Also drop the oddtk === ''check.
- Tests:
data-tree.test.tsusesBun.sleep(20)and a Bun.write/exists dance; make it deterministic (write the copy synchronously). The 60x backspace loops in tui-data are noise; use a helper. Confirm there is a test for a control byte in the Result screen, not only in the tree.
- Direction: the owner put pipeline work first; this is a feature pull. Keep it as it is, but do not add more.
This round fixes everything in @juniper's review. It changes no web pages and adds no features.
- SQL limit on the shell. The MCP
datatool (src/http/mcp/tools/data.ts) now appliessqlWindow(ctx.app, user.id)before it runs any SQL. The user is the verified identity behind the token, soforge data "SELECT …"now gets the same 20 runs a minute as the web page and the terminal. When the limit is hit, the tool returnsrate_limitedwith "too many queries: try again in N s", and the CLI prints that and exits 1. A new test intest/cli-data.test.tsruns 20 queries that succeed and checks the 21st is refused with the retry text and exit 1. It also checks that--tablebrowsing is not limited.docs/CLI.mdanddocs/features/data-browser.mdnow say the limit is shared across all doors. - Input caps in the terminal.
typing(max, initial)insrc/tui/screens/data.tsnow cuts the SQL prompt at 4000 characters and the filter at 200. The cap applies to typed keys, pasted text and the starting text. An empty or blank SELECT shows "Type one SELECT." and runs nothing, without using any of the person's SQL allowance. New tests cover both caps (typed and pasted) and the empty-SQL message. - Row count on the node. Table nodes now carry
total, and paging (turn()) reads that number instead of parsing the label with a regex. Thetk === ''check is gone. A test checks thattotalis set and that the label matches it. - Tests.
data-tree.test.tsnow copies the file withcopyFileSync, so there is no sleep and no exists-check. AnaskSqlhelper replaces the repeated 60-backspace loops intui-data.test.ts. The Result-screen control-byte test was already there ("a control byte in a cell cannot reach the terminal from a result…") and still passes.
scripts/gate.ts passes and bun x tsc --noEmit is clean. scripts/fast.ts named 1554 test files for the whole branch, too many to run here. Instead I ran the tests covering this change: data-tree, tui-data, cli-data, data-browser, data-query-limits, data-door and palette-roster. All pass.
To try it, run forge data <app> "select 1" 21 times within a minute: the 21st prints "try again in N s" and exits 1. In the terminal, open Data, press :, clear the line and press enter to see "Type one SELECT."
🤖 Generated with Claude Code
The terminal can now browse what the forge holds, much as the web data browser does: sources, their tables, the rows in each table, and read-only SQL. A person can see only what they could see on the web. A SQL prompt in the terminal counts against the same per-person budget as the web page and the MCP tool, 20 runs a minute, and prompts are capped.
This update merges main again. The only conflict was in src/http/mcp/tools/data.ts, and I took main's version. Both sides added the per-person SQL budget to the MCP data tool. This branch did it inline with sqlWindow. main did it with runSql from src/data/repo_scope.ts, which applies the same budget and also handles repo-scoped sources. main's version does everything this branch's did, so nothing from this side was lost. sqlWindow is still called directly by the terminal screen (src/tui/screens/data.ts) and the web page.
Checks run locally: bun scripts/gate.ts and bun x tsc --noEmit are clean. These test files pass (108 tests): api-data, data-door, data-browser, data-tree, data-query-limits, data-home, cli-data and tui-data.
To try it, connect with the command in FORGE_APP_SSH and open the data screen.
🤖 Generated with Claude Code
Earlier asks, checked against the diff (commit e3a0ca7a52bc is not in my clone, so I read the change as given):
- Tree cost and blocking reads: met.
Readscaches each open file's tables and each open table's page, and the screen rebuilds only on open, close, page turn, filter orr. A test shows a rebuild reads nothing again. - Paging: met.
nandpwork on a table or a row, andNode.totalcarries the count. - Tests for tree.ts,
h,y, the Result screen, CLI flags, exit 3,pickand the env argument: met. - CLI validation: met.
--pagemust be a whole number (exit 2), and--tabletogether with SQL is refused. - Per-person SQL budget on the shell: met. The MCP tool goes through
runSql, which callssqlWindow, and a test covers the 21st run. - Input caps in the terminal: met.
typing()cuts typed, pasted and prefilled text. - Control bytes: met.
cellLineis covered by a test. - Both doors and docs: met. There is a TUI screen and a
forge datacommand, the how-to is in docs/features/data-browser.md and docs/CLI.md, and README is untouched. - Preview:
/healthzanswers 200.
Non-blocking:
/filters only open lines, which the docs list under "Not done". A filter that opens matching nodes would be the next step.forge datamakes two MCP calls per invocation, a list and then the read. Cache or fold them if the CLI feels slow.
What I would do next is put the SQL prompt on the web page so both doors match.
The terminal Data screen is now a file tree, and
forge datareads the same data from a shell. I haven't run the full suite. Locally I rantsc --noEmit,scripts/gate.tsand 231 tests, covering the data, CLI and TUI files plustui-screensandtui-first, and all were green.scripts/fast.tssaid the diff reaches 1,544 test files, so I chose the covering files myself.What changed
src/tui/screens/data.tsshows repos → apps → environments →.dbfiles → tables → the first 50 rows.src/data/tree.tsbuilds it and reads only what is open. I added an optionalwherefield toSourceso the tree can group files by repo, app and environment.j/kmove,l/Enter open,hcloses or goes up,/filters,:opens a SQL prompt, andyyanks a row as JSON.SELECT * FROM <table> LIMIT 20when you're on a table. Results open in their own screen, whereyyanks a row too.forge data:forge datalists the databases you may read.forge data <app> [env]lists tables,--tableprints rows, andforge data <app> [env] "SELECT …"runs SQL. It prints a table, or--jsonfor an array of objects. It calls the MCPdatatool through a newForgeApi.tool, so the server enforces who sees what.docs/CLI.md,docs/ON-YOUR-MACHINE.mdanddocs/features/data-browser.mdare updated.test/tui-data.test.tscovers the tree for a writer, the filter, a refused write, and the JSON yank.test/cli-data.test.tscovers list, tables, rows with secrets masked,--json, a refused write, and a reader getting no access.Left out
:for more. I removed the oldn/ppaging.y) goes through the terminal's OSC 52 sequence. Tests check the JSON handed to it, not that a terminal received it.Try it: open the terminal app's Data command and press
l,j,/,:andy. Or runforge data <app> "select 1" --json.Closes #7581