Repository navigation
feat(tui): draw a two-provider pool as two panes, side by side - #406
Merged
Merged
Conversation
jgautheron
force-pushed
the
tui-provider-panes
branch
from
September 15, 2026 22:20
1599ba3 to
c5a5976
Compare
The account row's width budget reserved an 8-column type cell whatever the column held. Since KarpelesLab#392 a mixed pool writes the provider there, padded to the longest label present, and `Anthropic` is 9, so every row in a mixed pool ran 2 columns past the budget. fitLine then cut the tail: the reset label of the last bar, or the whole `$` tag behind a `⊘ Fable`. It only shows on a row that carries every reserved tag itself, which is how the budget's other slack hid it. The row and the budget now take the width from one helper.
…ding The row category keyed on readings alone, so an OAuth account that had not answered a request or a probe yet drew the API-key `Tok`/`Req` pair, and was budgeted with the metered rows: its empty bars sat at a different width from the subscription rows around it. A subscription meters Ses/Wk whether or not it has reported yet, so the category now also follows the auth kind.
jgautheron
force-pushed
the
tui-provider-panes
branch
from
September 15, 2026 22:23
c5a5976 to
cd20577
Compare
A pool holding Claude and Codex accounts rotates each provider on a cursor of its own, but ► read the manager's single `currentIndex`, which names whichever pool moved last. The other pool showed no current account at all, and switching one side with `s` took the marker away from the other. A mixed pool now marks the account each provider's cursor names, the same answer `currentAccounts` already gives in /teamclaude/status. That can be a shared API-key account, since a key serves either pool. The set is read once per frame. A single-provider pool is unchanged. Attach mode reads the same answer from the status payload. A name alone cannot tell a shared API key from a Codex login at the same address, so the payload now also carries `currentIndexes`, each pool's current account by position in `accounts`. Attach mode prefers it, falls back to the name matched on provider, and against a server that sends neither marks its one cursor as before.
A pool holding Claude and Codex accounts is one table today, each row naming its provider, so the table grows by the sum of both pools, and one person's two subscriptions, usually the same address, sit next to each other told apart by a single column. The dashboard now draws one pane per provider, Anthropic on the left and Codex on the right, so the table is as tall as the larger pool. The pane titles take the spacer line the table always had, so the split costs no height. A pane drops the type column, since its title names the provider, reserves route cells only for its own provider's routes, and caps its bars narrower. Each pane is laid out with the per-category budget the full table uses. Width is dealt out in the order a pane spends it: bars wide enough for their reset label, whole names, then wider bars. Both panes reach one step before either starts the next, a step that cannot be met in full is shared in proportion to what each pane still wants, so a pane with three bars and a `⊘ Fable` tag does not cut its names while the other pads. The table splits only when both panes reach the first step and keep every bar the rows draw in one column; otherwise it stays one column, now grouped by provider, which is also the order selection walks. Like the bars in one column, the split can change when a `⊘` tag appears near that width. A Codex plan need not state a five-hour limit. When the Codex accounts in a list have all reported and none states one, the list draws no Ses bar and starts at Wk. An account that has not reported yet keeps the column, so it does not come and go at startup.
jgautheron
force-pushed
the
tui-provider-panes
branch
from
September 15, 2026 22:28
cd20577 to
6599ee7
Compare
…tatus field The panes change what an operator sees on a wide terminal and the status payload gains a field, but neither was documented. usage.md now says when the account table splits into one pane per provider (two providers, 127 columns or more, every bar still drawable), that each pool has its own current marker, when the Ses bar is dropped, and what currentIndexes holds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts: # src/tui.js
MagicalTux
added a commit
to RuslanTar/teamclaude
that referenced
this pull request
Sep 20, 2026
The per-account switch-threshold tag moves into the row code KarpelesLab#406 introduced: _renderAcct still appends it, after master's reset-credit tag, and its width is now reserved in _listLayout's per-category `fixed` budget next to the blocked-family and spend tags. Because `fixed` feeds `span` and the pane `stages`, a side-by-side pane is sized to hold the tag or the split falls back to one column. accountBadges keeps master's `now` as its fourth argument, with the fleet threshold pair following it; SHARED_CONSTS and the sync-accounts config mirror carry both sides' entries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MagicalTux
added a commit
to rikbrown/teamclaude
that referenced
this pull request
Sep 20, 2026
…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>
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>
Merged
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
Sep 29, 2026
Upstream's two-pane layout (KarpelesLab#406) capped a pane's bars at 12 columns and, once both panes had every stage they asked for, shared the spare width between them. On an ultrawide that drew 12-column bars at each end of a gap of hundreds of columns: the Codex pane landed mid-screen, and the fleet panel, which follows what the rows occupy, landed at the far edge. Pane bars now grow to BAR_MAX, as the list's do, and width past the last stage stays unused on the right, so the panes and the panel sit together. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MagicalTux
pushed a commit
that referenced
this pull request
Oct 3, 2026
The two-pane layout (#406) capped a pane's bars at 12 columns and, once both panes had every stage they asked for, shared the spare width between them. On an ultrawide that drew 12-column bars at each end of a gap of hundreds of columns, with the Codex pane landing mid-screen. Pane bars now grow to BAR_MAX, as the list's do, and width past the last stage stays unused on the right, so the two panes sit together. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
rikbrown
added a commit
to rikbrown/teamclaude
that referenced
this pull request
Oct 7, 2026
Three things, and a fourth found on the way. THE FLEET SITS ALONGSIDE NOW. `[f]` was a swap: rows or aggregate, never both. On a wide terminal that was a false choice, so it is a cycle — split, full, off — starting at split, with the pools drawn as a panel to the right of the table. Below about a hundred columns there is no room for one and split degrades to rows only; never to fleet-only, because a dashboard whose default view contains no accounts is a bad first thing to see. The panel's column follows what the rows ACTUALLY occupy rather than what was reserved for them. Rows stop growing once the name column holds the longest name and every bar is at BAR_MAX, so on this fleet's 695-column terminal the table ends near column 160 and a panel pinned to the reservation sat 480 columns away from the thing it describes. The threshold for splitting at all is derived from the row budget rather than written down, because `fixed` grows with every general route, blocked-family tag and money tag on screen. A constant would drift from it and put a panel beside rows squeezed to two columns of bar — a row that has kept its shape and lost its meaning. WHAT STOPS EACH ROUTE. A route has no quota of its own; it spends its members' buckets, so "how much has the fable route left" is a min over three of them, not a bar. Each route now gets one line naming the bucket that binds first — highest utilisation among the family weekly (family routes only), the shared weekly and the 5h — over that route's members alone. Highest utilisation rather than soonest exhaustion on purpose: it needs no sampled history, so the line is honest from the first frame rather than blank for ninety minutes. A SEAT NO ROUTE REACHES IS NOT CAPACITY. Same error as counting a disabled seat, one step removed: quota that exists and that nothing will ever spend. Such a seat leaves the figures and stays in the tally, exactly as an unpriceable one does. Read off the RESOLVED membership `getRoutes()` returns, never raw config — a route listing no accounts means every account, not none. Deliberately narrow: the rule applies per provider pool, and only to a pool some route actually reaches. An ordinary Anthropic-only routing table names no Codex seat, and read fleet-wide that blanked the entire Codex block the moment one route existed. Where no route mentions a pool, routing is saying nothing about it rather than refusing it. It errs toward understating, which is the safe direction: a model matching no route at all still falls back to plain rotation, so a seat outside every route is not strictly unreachable. Understating spendable capacity risks adding an account nobody needed; overstating it risks being stopped mid-week by quota the dashboard promised. AND THE FOURTH THING, which is a live bug and predates all of this. The row budget's `fixed` never counted the provider column. It assumed the 7 columns a single-provider pool spends, while a mixed pool draws the widest provider label — `Anthropic`, 9 — so EVERY ROW OF A MIXED FLEET has been composed two columns past the terminal at every width, and fitLine has been eating the tail of each one silently. That is KarpelesLab#228 and KarpelesLab#234 a third time, in the one place neither of them thought to look. The row and the budget go through `_typeColW` now, so there is no second spelling of that width to drift. `routeFamily` moved from tui.js into quota-summary.js, so the route readout and the family bars resolve a family from one copy of the glob logic rather than two. `AccountManager.routeMembership()` is split out of `getRoutes` for the sampler: `getRoutes` clears expired quota windows as a side effect of testing availability, which is not something a request-path sampler should do, and two quota-probe tests said so. ALONGSIDE UPSTREAM'S PANES. Upstream now draws a two-provider pool as two panes (KarpelesLab#406), laid out by _listLayout, which replaces the inline row budget this change first split out. The rows here are that engine run against the width the panel leaves (_accountLines), and the split's floor is the single list's first _listLayout stage (_fleetSplit, renamed from _splitLayout, which upstream now uses for the panes). The panel keeps its place: panes are an arrangement of the same bars, so the rows draw as panes only when the column beside the panel still fits both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
A pool holding Claude and Codex accounts is one table today. Since #392 each row names its provider, which tells the two apart, but the table still grows by the sum of both pools, one person's two subscriptions (usually the same address) sit side by side, and
►marks only one pool's current account: the manager's singlecurrentIndexnames whichever provider moved last, so the other pool shows none, andson one side takes the marker away from the other.What changed
Anthropicis 9, 7 were reserved), cutting the last tag.Ses/Wkinstead ofTok/Req.►per pool, from each provider's cursor. Status gainscurrentIndexesso attach mode can tell same-named accounts apart.Ses.