Skip to content

feat(tui): draw locally-served accounts last - #376

Merged
MagicalTux merged 2 commits into
KarpelesLab:masterfrom
rikbrown:feat/local-upstream-last
Sep 15, 2026
Merged

MagicalTux merged 2 commits into
KarpelesLab:masterfrom
rikbrown:feat/local-upstream-last

Conversation

@rikbrown

Copy link
Copy Markdown
Contributor

An account whose upstream is a process on this machine — a local translating proxy in front of another vendor — is infrastructure rather than a seat the fleet rotates between, so it reads as noise wedged among the accounts that do rotate. Config order cannot hold it at the end on its own, because a newly added account is appended after it and puts it back in the middle.

isLocalUpstream (provider.js, next to upstreamFor) keys on the account's upstream resolving to loopback. A remote third-party backend — DeepSeek, GLM — keeps a public host and is not caught, so only a genuinely local backend moves.

Display only. selIdx, currentIndex, session pins and route entries all stay manager indices, so nothing about selection or routing moves with the rows. _keySelect walks the order as drawn while still storing an index, and there is a test pinning exactly that: with a local account in the middle, pressing down from the first row lands on the account below it, not on the one that moved.

Alternatives considered: pairing accounts against a declared local process does not generalise, since such a declaration carries a command rather than a port, and the port sits inside its argv where every program spells it differently. A loopback upstream says the same thing directly, and says it for a hand-started process too.

Tests: 6 new in test/tui-account-order.test.js, rendered rather than unit-tested — the order rows are drawn in is the property, and only render() decides it. Full suite green; npm run typecheck clean; strict-mode diagnostics unchanged against master for both touched files (provider.js 16, tui.js 225), so the ratchet stays green.

🤖 Generated with Claude Code

rikbrown and others added 2 commits September 12, 2026 14:47
An account whose upstream is a process on this machine — a local
translating proxy in front of another vendor, say — is infrastructure
rather than a seat the fleet rotates between, so it reads as noise wedged
among the accounts that do rotate. Config order cannot hold it at the end
on its own: a newly added account is appended AFTER it and puts it back
in the middle.

Sort on whether the account's upstream resolves to loopback. A remote
third-party backend (DeepSeek, GLM) keeps a public host and is not
caught, so only a genuinely local backend moves.

Display only: selIdx, currentIndex, session pins and route entries all
stay manager indices, so nothing about selection or routing moves with
the rows. Up/down walk the order as drawn while still storing an index.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@MagicalTux
MagicalTux merged commit c9d1b58 into KarpelesLab:master Sep 15, 2026
5 checks passed
rikbrown added a commit to rikbrown/teamclaude that referenced this pull request Sep 15, 2026
The TUI ordering this commit used to carry went upstream as KarpelesLab#376 (c9d1b58)
and now arrives with the rebase instead, review annotations included. The
fork-only docs/openai.md section is the part upstream never took, so it is
all that remains here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rikbrown added a commit to rikbrown/teamclaude that referenced this pull request Sep 15, 2026
Rebases the fork onto upstream master 1779db6 (1.1.20 plus KarpelesLab#374-KarpelesLab#403).
No new fork feature of its own: this ships upstream's 13 commits and the
reshaping the rebase required.

Upstream brings a version label in the dashboard header, provider names
on the account rows of a mixed pool, Codex quota fixes across the 5h and
weekly windows, spreading for requests that carry no session id, and
outcome accounting on the same classification path every routing
decision already read.

Two fork commits dissolved into upstream, both merged there today: the
dashboard key prompt (KarpelesLab#381), and the expiring Codex header fixture,
which upstream fixed independently as KarpelesLab#402. The locally-served account
ordering went up as KarpelesLab#376, so only its fork-only documentation remains
here; and the header commit keeps the branding alone, now that upstream
resolves and draws the version itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rikbrown added a commit to rikbrown/teamclaude that referenced this pull request Sep 19, 2026
The TUI ordering this commit used to carry went upstream as KarpelesLab#376 (c9d1b58)
and now arrives with the rebase instead, review annotations included. The
fork-only docs/openai.md section is the part upstream never took, so it is
all that remains here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rikbrown added a commit to rikbrown/teamclaude that referenced this pull request Sep 19, 2026
Rebases the fork onto upstream master 1779db6 (1.1.20 plus KarpelesLab#374-KarpelesLab#403).
No new fork feature of its own: this ships upstream's 13 commits and the
reshaping the rebase required.

Upstream brings a version label in the dashboard header, provider names
on the account rows of a mixed pool, Codex quota fixes across the 5h and
weekly windows, spreading for requests that carry no session id, and
outcome accounting on the same classification path every routing
decision already read.

Two fork commits dissolved into upstream, both merged there today: the
dashboard key prompt (KarpelesLab#381), and the expiring Codex header fixture,
which upstream fixed independently as KarpelesLab#402. The locally-served account
ordering went up as KarpelesLab#376, so only its fork-only documentation remains
here; and the header commit keeps the branding alone, now that upstream
resolves and draws the version itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MagicalTux added a commit that referenced this pull request Sep 20, 2026
* feat(tui): set the order the account list draws in

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. #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>

* test(tui): hold the reorder to the rows, and to nothing else

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>

* docs: the account list has an order of its own now

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>

* fix(tui): keep account reordering inside provider groups, debounce its save, carry it to attach

After #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>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Mark Karpeles <magicaltux@gmail.com>
@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 TUI ordering this commit used to carry went upstream as KarpelesLab#376 (c9d1b58)
and now arrives with the rebase instead, review annotations included. The
fork-only docs/openai.md section is the part upstream never took, so it is
all that remains here.

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