Repository navigation
Faster lists, no wait for Cursor, clearer narrow and TUI output - #29
Merged
Merged
Conversation
The joins that put a Cursor subagent's billed usage on its session, and that keep an IDE composer's CLI copy from being billed twice, were loaded in a sync.Once that dropped SQL and scan errors and kept the empty result for the life of the DB, the TUI's included. A failure then showed totals short of every subagent's usage, or a conversation billed twice, as if nothing had gone wrong. The joins now load together, the error reaches the command, and a failed load is tried again by the next caller. Seven row loops in the dashboard, session detail, account totals, reprice and reconcile closed their rows without checking the error that ended the loop, so a read cut short counted as the whole. They check it now. Tested: go test ./...; go vet ./...
The session list summed every response in the index, twice (once for totals, once for the model list), and only then applied the account, folder, tool, project and search filters in Go. It now lists sessions from their files first, filters them, and sums responses in one pass per session and model for the ones left, through the (account, session_id) index. The model filter, which needs the responses, runs after that pass. On a 418k-response index: the default 30-day list 1.08s to 0.44s, -a with one account 1.08s to 0.08s, --dir 1.13s to 0.09s, accounts 1.42s to 0.70s, stats 2.07s to 1.30s. An all-time list stays at about 1.1s; it reads every response either way, and a full scan measured slower than the index. Models whose cost ties (Cursor's, which have none in the transcript) came in whatever order the sort left them; they now come by how often each answered, then by name. A Cursor row with billed usage orders its models by billed cost and lists every billed model, its subagents' included, instead of putting only the costliest billed one in front. Outputs on the real index are otherwise unchanged apart from float rounding. Rejected: a per-session usage table kept by the indexer | responses are deduplicated across files, repriced, and adjusted by Cursor's ledger, so keeping sums right would touch every write path Rejected: applying -n before summing | the footer totals every matched session Tested: go test ./...; JSON of sessions, accounts, stats and show compared before and after on a frozen copy of the real index
Every command listed every composer in Cursor's state.vscdb before it showed anything, even with -a naming a Claude account. On a 3.2 GB database that was 0.2s with a warm cache and up to 1.2s cold, most of an index run, and Cursor was usually not running at all. A run now compares a stamp of the database and its WAL (sizes, times, the database header with its change counter, the WAL header with its salt) with the one saved by the last full read, and when they match keeps the composers the index has. The headers catch commits a coarse file clock would miss. A row a newer parser must read again forces a full read, the stamp is saved only by a run with no read errors so a failed composer is retried, and --rebuild always reads. An index run with nothing new went from 0.22s to 0.08s on this machine. Rejected: refreshing only the accounts -a names | a bare id still needs every account to find its copies, and the IDE's composers decide which Cursor CLI sessions are its copies and must not be billed twice Tested: go test ./... (a test counts the listings: none for an unchanged database, one after a write, one with --rebuild); index runs on a copy of the real index
TestSessionsFiltersAndTitles wrote a session and its copy in another account within the same millisecond on a busy machine. The file created first owns a shared response and birth times are kept in milliseconds, so on a tie the owner was whichever of the two index workers wrote first, and the copy sometimes owned the original's tokens. The fixture now waits for the next millisecond before writing the copy, as a real copy would be.
Once the usage cache was 15 minutes old, every command that could show a Cursor account waited for Cursor's dashboard before printing anything, up to the 45-second read timeout. A command that finds a stale cache with usage in it now shows the cache and starts `ccli cursor-refresh` detached, in a session of its own, so the next command has fresh figures. That command takes a lock file created exclusively, so commands that each find the cache stale start one read between them; a lock older than two minutes counts as left behind. Only a command with no usable cache still waits. Showing older figures is said where it matters: usage more than a day old, or an update that failed, gets a line on stderr instead of passing for current. Rejected: a time limit on the foreground read | a weekly full read takes longer than any limit worth waiting for, and a read cut off is lost Tested: go test ./... (tests replace the process start); a stale cache on a copy of the real data printed in 195ms, and the detached read updated it and removed its lock
In a narrow pane (48 columns in tmux) a session row wrapped over three or four screen lines, and cost no longer lined up with its account. Shrinking the flexible columns cannot help there: when, account, id, a short title and cost alone need about 59 columns. When the table cannot fit with every flexible column at its narrowest, sessions now take two lines each: when, account, id and tag with the cost at the right edge, then the title and as much of project, model, active time and tokens as fits. The totals and the legend break between items instead of mid-word. The list of copies a bare id prints uses the same rendering. Wide terminals are unchanged. Rejected: dropping columns until a row fits | even the essential columns are wider than the pane Tested: go test ./...; sessions and a duplicate-id show at 48 and 160 columns on a copy of the real index
…very account With a session's summary in focus, the footer offered t, T, c, o and y. The summary follows none of the first three, and o and y did nothing, without a word. The footer now comes from the selected row: t, T and c only for a transcript (a turn or an agent's log), o only for a row with agents, y only for a row with a prompt. Pressing o or y on a row without them says so. Hiding and tagging go by session id, so they reach the copies in other accounts, but the TUI row reads as one account's. The hide question now says "in every account", and the tag prompt reads "tag (all accounts):". Tested: go test ./... (a footer test for a session summary and for an agent's transcript)
…ery run Shares are built in a map, and two with the same cost and tokens (common for projects with no priced usage) came out in a different order from one run of stats to the next. They now fall back to name order.
Two commands that both found a left-behind lock could each take it, and a holder finishing after its lock was taken over removed the new owner's. The lock file now holds a token, and release removes it only while it is still the holder's. The narrow session list also keeps its title width at one cell or more in a terminal of a few columns. Tested: go test ./... (a late release no longer frees the lock that took over)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-ups from a review of performance and UI/UX, one commit each.
sync.Oncethat dropped SQL errors and kept the empty result for the life of the DB. They now return the error, and a failed load is retried. Seven row loops that closed without checkingrows.Err()now check it.filesfirst and are filtered, then responses are summed in one pass per session and model through the(account, session_id)index. Models that tie on cost come by call count, then by name. Cursor rows order models by billed cost and list their subagents' billed models.--rebuildall force a full read.ccli cursor-refresh, with a lock file so only one runs. Only a command with no cache waits. Usage over a day old, or a failed update, is said on stderr.oorywith nothing to act on says so. The hide question and the tag prompt say they apply in every account.Timings
On a 418k-response index, with the index refresh included:
sessions(30 days)sessions -a <one account>sessions --dir <folder>show <id>accountsindexwith nothing newA list over all time still reads every response, in about 1.1s.
Not done
-a. A bare id needs every account to find its copies, and the IDE's composers decide which Cursor CLI sessions must not be billed twice. Skipping the unchanged IDE database removes most of that cost anyway.Test plan
go test ./...,go vet ./...sessions(8 filter sets),accounts,statsandshow, compared before and after on a frozen copy of the real index. Only float rounding and the intended model-order changes differ.sessionsand a duplicate-idshowat 48 and 160 columns.