Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ teamclaude priority <name> --last

`login`, `import`, `enable`, `disable` and `priority` notify a running server to reload, so credential, priority and enable/disable changes are picked up live; the same reload (POST `/teamclaude/reload`, or **R** in the TUI) also applies hand edits to an account's `upstream`/`modelMap`. Account **removals** made on disk still need a restart, because a reload never drops a running account; removing one from the TUI or through the [MCP endpoint](usage.md#mcp-endpoint)'s `remove_account` takes effect at once.

Accounts can also be added and removed from the TUI settings screen: **`g`** → **Add account** / **Remove account**.
Accounts can also be added, removed and reordered from the TUI settings screen: **`g`** → **Add account** / **Remove account** / **Reorder accounts**.

**Reorder accounts** sets the order the account list is drawn in — `↑`/`↓` pick an account, `←`/`→` move it up and down, each move saved as you make it. It writes a `displayOrder` on the entry and touches nothing else: an account keeps its place in the `accounts` array, so route pins, session pins and `TC_ACCT` all go on naming the same accounts, and rotation order stays `priority`'s business alone. An account with no `displayOrder` — every account, until the first time you arrange them, and every one added afterwards — lists after the ones that have one, which is where a newly added account appeared anyway. A [third-party backend](#third-party-backend-accounts) served by a local process is infrastructure rather than a seat to rotate between: the TUI keeps those at the end of the list, and the screen leaves them there.

## The `id` field

Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ While the config is being rewritten — by the server rotating a refresh token,
| `accounts[].orgUuid` / `orgName` | Organization the account is scoped to — lets one email hold multiple org accounts |
| `accounts[].priority` | Rotation preference, lower = preferred (default 0) |
| `accounts[].switchThreshold` | Per-account override of the fleet-wide `switchThreshold`: a number, or a per-bucket table like `{ "unified7dFable": 0.8 }`. Resolution is per bucket — the account table's entry for that bucket, else the account's own `default` (or bare number), else the fleet's `switchThreshold` for that bucket — so a table without `default` only overrides the buckets it lists and inherits the rest from the fleet. Still a *preference*, exactly like the fleet setting: the all-exhausted revalidation probe can override it too. That is what keeps it a different setting from the hard `accounts[].maxUsage` cap below. No CLI editor — hand-edit and reload, same as `maxUsage`. Applied live on reload. See [Per-account thresholds](quota.md#per-account-thresholds) |
| `accounts[].displayOrder` | Where the account sits in the TUI's account list, lowest first; unset lists after every account that has one. **Display only** — it decides nothing about which account rotation spends, which is `priority` above. Set it from the TUI (**`g`** → **Reorder accounts**), or by hand; applied live on reload |
| `accounts[].maxUsage` | Hard per-account usage cap: a number, or a per-bucket table like `{ "unified7d": 0.6, "unified7dFable": 0.8 }` (same keys as `switchThreshold`; `default` covers unlisted buckets, anything else is uncapped). At the cap the account receives **no** requests — rotation skips it, the all-exhausted revalidation probe skips it, and a pin gets the exhausted answer. Model-scoped like the thresholds, applied live on reload. See [Per-account usage caps](quota.md#per-account-usage-caps) |
| `accounts[].disabled` | If `true`, the account is excluded from rotation until re-enabled |
| `accounts[].upstream` | Alternative upstream base URL for this account (e.g. `https://api.deepseek.com/anthropic`). Overrides the global `upstream` for this account only — see [third-party backends](accounts.md#third-party-backend-accounts) |
Expand Down
4 changes: 3 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,15 @@ Warning: "me@example.com" is disabled, so requests will not route to it until th
| `d` | Enable/disable an account |
| `p` | Refresh quota on all accounts (one-shot probe of the zero-spend usage endpoint) |
| `R` | Reload accounts from config |
| `g` | Settings (threshold, quota probe, routing, add/remove accounts, sx.org) |
| `g` | Settings (threshold, quota probe, routing, add/remove/reorder accounts, sx.org) |
| `q` | Quit |

In selection mode, use `j`/`k` or the arrow keys to navigate, `Enter` to confirm, `Esc` to cancel.

The settings screen is a list, not a set of letter shortcuts: `↑`/`↓` move between rows, `←`/`→` change the value in place (threshold by 1%, probe by 30s, modes cycle), `Enter` opens a row that needs typing or a sub-screen, `Esc` goes back.

**Reorder accounts** opens the account list with the same two pairs of keys and one extra job for them: `↑`/`↓` pick the account, `←`/`→` move *that account* up and down the list, `Enter` or `Esc` goes back. Every move applies as you make it and is written a moment after the keys stop (or on leaving the screen), so there is nothing to confirm and nothing to cancel. An account moves among the accounts of its own provider: a mixed Claude and Codex fleet is drawn grouped by provider, so a move that would cross into the other group does nothing. This is the order the list is **drawn** in and nothing else — rotation order is [`priority`](routing.md#choosing-an-account), which the screen never touches. `teamclaude attach` draws the same order: each account in `/teamclaude/status` carries its `displayOrder` (`null` until it has been placed).

## Run Claude Code through the proxy

```bash
Expand Down
11 changes: 11 additions & 0 deletions src/account-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,13 @@ function makeAccount(acct, index) {
hasClaudeMax: acct.hasClaudeMax ?? null,
hasClaudePro: acct.hasClaudePro ?? null,
priority: acct.priority || 0,
// Where this account sits in the list the TUI draws, arranged by the
// operator from the settings screen. Presentation only: nothing but
// _displayOrder in tui.js reads it, and it is emphatically NOT `priority`
// above — that one decides which account rotation spends next, and the two
// answer different questions about the same fleet. `null` means never
// placed, which lists after every account that has been.
displayOrder: Number.isFinite(acct.displayOrder) ? acct.displayOrder : null,
disabled: acct.disabled || false,
maxUsage: acct.maxUsage ?? null,
// Per-account switchThreshold override (issue #409) — a rotation
Expand Down Expand Up @@ -4110,6 +4117,10 @@ export class AccountManager {
provider: providerOf(a),
orgName: a.orgName || null,
priority: a.priority || 0,
// The attached TUI spreads these fields onto its own account objects
// and sorts its rows by this one, so without it `teamclaude attach`
// draws array order beside a server TUI drawing the arrangement.
displayOrder: a.displayOrder ?? null,
disabled: a.disabled || false,
maxUsage: a.maxUsage ?? null,
// Raw per-account override (issue #409), same shapes as the fleet-wide
Expand Down
8 changes: 8 additions & 0 deletions src/sync-accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ export async function syncAccountsFromDisk(diskConfig, memConfig, accountManager
}
if (diskAcct.name && mgr.name !== diskAcct.name) mgr.name = diskAcct.name;
if (diskAcct.priority != null && mgr.priority !== diskAcct.priority) mgr.priority = diskAcct.priority;
// A list position edited on disk applies on reload for the same reason a
// priority edit does, and `null` rather than `??` so deleting the field
// puts the account back among the unplaced instead of leaving it stuck at
// the position it last held. Display only — see makeAccount.
mgr.displayOrder = Number.isFinite(diskAcct.displayOrder) ? diskAcct.displayOrder : null;
// A cap edit applies live for the same reason priority does: it is an
// operator decision about a running fleet, and waiting for a restart to
// honour a budget defeats the budget.
Expand Down Expand Up @@ -138,6 +143,9 @@ export async function syncAccountsFromDisk(diskConfig, memConfig, accountManager
if (diskAcct.maxUsage != null) cfgAcct.maxUsage = diskAcct.maxUsage; else delete cfgAcct.maxUsage;
if (diskAcct.switchThreshold != null) cfgAcct.switchThreshold = diskAcct.switchThreshold; else delete cfgAcct.switchThreshold;
if (diskAcct.priority != null) cfgAcct.priority = diskAcct.priority; else delete cfgAcct.priority;
// The TUI's reorder writes this key onto the entry, so after one
// arrangement every entry carries a value for a hand edit to lose to.
if (Number.isFinite(diskAcct.displayOrder)) cfgAcct.displayOrder = diskAcct.displayOrder; else delete cfgAcct.displayOrder;
if (diskAcct.disabled) cfgAcct.disabled = true; else delete cfgAcct.disabled;
}
// Pick up enable/disable toggles; re-enabling clears a stuck error state.
Expand Down
Loading
Loading