Skip to content

feat(tui): set the order the account list draws in - #418

Merged
MagicalTux merged 7 commits into
KarpelesLab:masterfrom
rikbrown:rikbrown/tui-account-order
Sep 20, 2026
Merged

MagicalTux merged 7 commits into
KarpelesLab:masterfrom
rikbrown:rikbrown/tui-account-order

Conversation

@rikbrown

Copy link
Copy Markdown
Contributor

The problem

A new account is appended to accounts, so it lands at the bottom of the TUI's
list and stays there. The only way to move it was to hand-edit the accounts
array — which is the one edit that array does not tolerate. A manager index
addresses an account in route pins, session pins, currentIndex, TC_ACCT and
the disable/switch CLI paths, so permuting the array silently repoints every one
of them at a different account. _keySelect's own comment says as much.

priority looks like the answer and is not. It is rotation preference, read
throughout account-manager.js at selection time and with its own CLI command.
The order a fleet reads well in and the order it should be spent in are
different questions: a fleet usually carries exactly one non-default priority —
a deliberately deprioritised local backend — and nobody wants tidying the
display to re-rank routing. So one of the two orders was controllable from the
UI and it was the wrong one.

The design

An explicit per-account display field, displayOrder, that _displayOrder()
sorts the rows by. The array is never permuted; every index everything else
holds keeps naming the account it named.

  • Settings → Reorder accounts opens the account list with ↑/↓ picking an
    account and ←/→ moving that account up and down. selIdx names the
    account being dragged, so the cursor rides with it rather than with the row
    number. Each move is saved as it is made, which is why Enter and Esc both
    just leave — there is no pending change for one to commit and the other to
    discard, and offering "cancel" would promise an undo this screen does not have.
  • priority is untouched. Nothing in this branch writes it. A test asserts
    that, on the account, on its config entry, and in everything the save puts on
    disk.
  • No migration. An account with no displayOrder has never been placed and
    sorts after every account that has one — which is exactly where a new account
    appeared before this existed. An existing install sees no reshuffle on
    upgrade. The comparison is ra === rb ? a - b : ra - rb rather than
    ra - rb || a - b on purpose: two Infinity ranks subtract to NaN.
  • feat(tui): draw locally-served accounts last #376 still wins. Locally-served accounts are drawn last because a
    translating proxy in front of another vendor is infrastructure rather than a
    seat to rotate between — a category, not a preference. _displayOrder() still
    partitions on isLocalUpstream before it looks at the arrangement, those
    accounts are never given a displayOrder, and the reorder cursor steps over
    the rows it cannot move.
  • Round trip. makeAccount normalises the field (a non-finite value reads as
    unplaced), syncAccountsFromDisk carries a hand edit onto the running account
    on reload the way a priority edit already is, and the value survives
    mergeAccountsForSave — the merge is where a field the module does not know
    about gets dropped, so a reorder that vanished on restart would have looked
    like the TUI never saved it.

What changed

File
src/account-manager.js displayOrder on the account built from a config entry, normalised, null when unset
src/sync-accounts.js a disk edit to it applies on reload
src/tui.js listRank, the sort in _displayOrder(), _arrangeable(), _doMoveAccount(), the settings row, the reorder select mode and its footer
test/tui-reorder.test.js 17 cases, most of them asserting what did not change
docs/accounts.md, docs/usage.md, docs/configuration.md the screen, its keys, and the field beside priority

Checks

npm test test/ — 1882 pass, 0 fail. npm run lint and npm run typecheck
clean. node scripts/typecheck-strict.mjs --base 7b1e92d0 — 1831 diagnostics on
both sides, no file grew.

🤖 Generated with Claude Code

rikbrown and others added 7 commits September 18, 2026 12:30
New accounts land at the bottom of the list, and the only way to move one
was to hand-edit the `accounts` array in the config — which is the one edit
that array does not tolerate. A manager index addresses an account in route
pins, session pins, `currentIndex`, `TC_ACCT` and the disable/switch CLI
paths, so permuting the array silently repoints every one of them at a
different account.

So the rows get a sort key instead. `displayOrder` on an account is
presentation and nothing else: each account keeps the slot it has held since
startup, and _displayOrder sorts the rows by the field before drawing them.
Settings → Reorder accounts opens the list with ←→ moving the selected
ACCOUNT rather than the cursor, each move saved as it is made.

Emphatically not `priority`, which is one field over and answers a different
question: which account rotation spends next. A fleet usually carries one
non-default value there — a deliberately deprioritised local backend — and
deriving it from where a row sits on screen would re-rank routing as a side
effect of tidying the display.

Locally-served accounts are left out of it. KarpelesLab#376 draws those last because a
translating proxy in front of another vendor is infrastructure rather than a
seat to rotate between, and that is a category rather than a preference, so
it stays ahead of the arrangement: _displayOrder still partitions on
isLocalUpstream first, those accounts hold no `displayOrder`, and the
reorder cursor steps over the rows it cannot move.

An account with no value has never been placed, and sorts after every
account that has one. That is where a new account already appeared, so the
answer to "where does the one I just logged in with go" does not change with
the feature and there is nothing to migrate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Most of what this feature must do is leave things alone, so most of these
assert what did not move: the account array is not permuted, no manager
index changes, the current account stays current, an index standing in for a
route pin still names the account it named, and `priority` is never written
— on the account, on its config entry, or in anything the save puts on disk.

A locally-served account gets two of its own. It stays at the end of the
list however the rows above it are arranged, it is never given a
`displayOrder`, and the reorder cursor does not stop on its row, since ←→
there would do nothing.

The round trip goes through mergeAccountsForSave rather than around it. That
function merges an in-memory entry over its disk row, and a field it did not
know about is exactly the kind that gets dropped there, so a reorder that
vanished on restart would have looked like the TUI never saved it. The two
ends of the same trip — makeAccount normalising what it reads and
syncAccountsFromDisk carrying a hand edit onto a running account — are
asserted against the real AccountManager rather than the harness stand-in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The distinction worth writing down is the one the code spends its comments
on: this sets the order the list is DRAWN in, and rotation order is still
`priority` alone. Said in accounts.md where the settings screen is
described, in usage.md where its keys are, and in the config table, where
the two fields now sit one row apart and the reader is entitled to ask what
the difference is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s save, carry it to attach

After KarpelesLab#406 the list is grouped by provider before anything else, so _displayOrder now sorts by provider, then local-upstream-last, then displayOrder, then manager index, and a move that would cross a provider group is a no-op that neither renumbers nor saves. The reorder save is debounced so a held arrow key is one locked config write, flushed on leaving the screen and in stop(). syncAccountsFromDisk mirrors a disk displayOrder onto the in-memory config entry, since the save stencil is {...diskAcct, ...live} and the stale key otherwise reverted a hand edit on the next save. getStatus emits displayOrder per account so `teamclaude attach` draws the same order as the server's own TUI. Tests cover the refused cross-group move as rendered, the single write per run of moves, the status field and the reload-save-reload round trip.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts:
#	docs/configuration.md
@MagicalTux
MagicalTux merged commit 8880063 into KarpelesLab:master Sep 20, 2026
5 checks passed
@MagicalTux MagicalTux mentioned this pull request Sep 20, 2026
MagicalTux added a commit that referenced this pull request Sep 20, 2026
Thirty-six commits since 1.1.20. Several change what a client sees on a
failure, so read the first section before upgrading a shared deployment.

Behaviour changes
  #439 a 401 from upstream is no longer relayed to the client: the request
       fails over like a 403, and an API-key account or an OAuth account with
       no refresh token leaves rotation (it used to be picked again on every
       request). With nothing left the client gets the proxy's own error
  #439 a path under /teamclaude/ that no control route claims — a typo, or the
       wrong verb — answers 404 locally instead of being forwarded upstream
       under a fleet credential
  #429 the synthetic 429's retry-after is the real reset of the windows
       blocking the request's candidate accounts, not a flat 60s, and the
       message counts only those candidates; #408 names accounts that need a
       re-login instead of calling them "at quota"
  #438 session pins are per conversation (session id plus a digest of the
       first message), so a session's subagents spread across accounts.
       `sessions.items[].id` in status is the composite key, load is counted
       per conversation, and a persisted concurrency cap re-learns
  #378 a `thread: continue` bound for a per-account third-party upstream is
       refused with the 400 Anthropic gives, so the client resends the whole
       conversation; `messageThreads: true` opts a relay out
  #434 a 429 whose x-codex-* headers show a spent account-wide window holds
       the account like an Anthropic rejection; a spent model-scoped bucket
       only moves the request
  #437 a Codex response head is awaited for five minutes (Anthropic unchanged)
  #411 idle keep-alive connections are held 120s on both listeners
  #389 with session distribution on, requests carrying no session id rotate on
       a cursor of their own instead of all resting on the current account
  #405 `defaultClientMode` ("mitm" | "base-url") sets what `run` and `env` do
       without a flag; `--mitm` / `--no-mitm` decide per launch, and in
       base-URL mode `env` unsets a stale proxy export naming this proxy
  #439 route `--bucket` is validated; an array `switchThreshold` reads as the
       default with one line saying so

Features
  #419 an MCP management endpoint at POST /teamclaude/mcp, off unless
       `proxy.mcp` is "read" or "full"; a named client key is read-only even
       in full mode, and with no proxy key it serves only this machine
  #428 per-account `switchThreshold`, a number or a per-bucket table
  #406 a Claude+Codex pool is drawn as two panes on a wide terminal, each with
       its own current marker; #392 names the provider in a mixed list; #418
       lets the operator arrange the list (`displayOrder`); #376 draws
       loopback-served accounts last; #435 shows the percentage beside a bar's
       countdown; #394 shows the running version in the header
  #430 free Codex rate-limit reset credits in status, the TUI and the dashboard
  #385 `proxy.terminalOnly` tunnels chatgpt.com so ChatGPT Desktop stays out

Fixes
  #404 a TUI paint can no longer block the proxy (stdout non-blocking, frames
       dropped while the terminal is behind); #410 a dead terminal no longer
       takes the proxy with it, and SIGHUP shuts down cleanly
  #433 token usage is booked from Codex Responses streams
  #386 #387 #388 the Codex five-hour window is read from the model-scoped
       family and the usage probe, and extra limits are named from their entries
  #431 a headerless 429 that follows the request is retried once
  #432 #439 startup and collaborator log lines reach the TUI's activity pane
       and log file instead of the covered terminal
  #415 #403 #439 reload mirrors `priority`, `disabled`, `stripRequestFields`
       onto the config entry, and a reload during a removal does not re-add it
  #439 the Host check uses the address actually bound; sx.org calls time out
  #381 the dashboard polls status before asking for a key
  #403 session outcome accounting classifies the decoded path; account names in
       daemon log lines are sanitised

Tooling
  #371 #372 #373 `npm run typecheck` (tsc over the JS sources) in CI, with a
       strict-mode ratchet: per-file strict diagnostics may not grow past the
       pre-merge commit (2006 at introduction, 1735 now)
  #401 docker workflow actions bumped

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
rikbrown added a commit to rikbrown/teamclaude that referenced this pull request Oct 7, 2026
The reorder case joins the SCREENS table, which already holds every mode's
footer line to the terminal's arithmetic at ten widths from 40 columns up.
The reorder behaviour itself is covered by upstream's tui-reorder.test.js
(KarpelesLab#418).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants