@victor-software-house/pi-multicodex 1.0.10 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -25
- package/account-manager.ts +19 -0
- package/commands.ts +112 -24
- package/package.json +11 -3
- package/status.ts +44 -23
package/README.md
CHANGED
|
@@ -45,7 +45,9 @@ Run the extension directly during local development:
|
|
|
45
45
|
pi -e ./index.ts
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
##
|
|
48
|
+
## Current commands
|
|
49
|
+
|
|
50
|
+
These commands reflect the current shipped implementation:
|
|
49
51
|
|
|
50
52
|
- `/multicodex-use [identifier]`
|
|
51
53
|
- Use an existing managed account, or start the Codex login flow when the account is missing or the stored auth is no longer valid.
|
|
@@ -55,6 +57,57 @@ pi -e ./index.ts
|
|
|
55
57
|
- `/multicodex-footer`
|
|
56
58
|
- Open an interactive panel to configure footer fields and ordering.
|
|
57
59
|
|
|
60
|
+
## Planned command migration
|
|
61
|
+
|
|
62
|
+
The next user-facing milestone is a command-surface migration to one command family:
|
|
63
|
+
|
|
64
|
+
- `/multicodex`
|
|
65
|
+
- open the main interactive UI
|
|
66
|
+
- `/multicodex show`
|
|
67
|
+
- show runtime state and active-account summary
|
|
68
|
+
- `/multicodex use [identifier]`
|
|
69
|
+
- choose or activate an account
|
|
70
|
+
- `/multicodex footer`
|
|
71
|
+
- open footer settings
|
|
72
|
+
- `/multicodex rotation`
|
|
73
|
+
- open rotation settings
|
|
74
|
+
- `/multicodex verify`
|
|
75
|
+
- verify runtime health and local storage access
|
|
76
|
+
- `/multicodex path`
|
|
77
|
+
- show config and storage paths
|
|
78
|
+
- `/multicodex reset`
|
|
79
|
+
- reset selected extension state
|
|
80
|
+
- `/multicodex help`
|
|
81
|
+
- print compact usage text
|
|
82
|
+
|
|
83
|
+
Migration policy:
|
|
84
|
+
|
|
85
|
+
- the old top-level commands will be removed once `/multicodex` is ready
|
|
86
|
+
- no backward-compatibility aliases are planned
|
|
87
|
+
- README, roadmap, tests, and release notes will move together in the same change
|
|
88
|
+
|
|
89
|
+
## Architecture overview
|
|
90
|
+
|
|
91
|
+
The implementation is currently organized around these modules:
|
|
92
|
+
|
|
93
|
+
- `provider.ts`
|
|
94
|
+
- overrides the normal `openai-codex` provider path
|
|
95
|
+
- mirrors Codex models and installs the managed stream wrapper
|
|
96
|
+
- `stream-wrapper.ts`
|
|
97
|
+
- account selection, retry, and quota-rotation path during streaming
|
|
98
|
+
- `account-manager.ts`
|
|
99
|
+
- managed account storage, token refresh, usage cache, activation logic, and auth import sync
|
|
100
|
+
- `auth.ts`
|
|
101
|
+
- reads pi's `~/.pi/agent/auth.json` and extracts importable `openai-codex` OAuth state
|
|
102
|
+
- `status.ts`
|
|
103
|
+
- footer rendering, footer settings persistence, footer settings panel, and footer status refresh logic
|
|
104
|
+
- `commands.ts`
|
|
105
|
+
- current slash command registrations and account-selection flows
|
|
106
|
+
- `hooks.ts`
|
|
107
|
+
- session-start and session-switch refresh behavior
|
|
108
|
+
- `storage.ts`
|
|
109
|
+
- persisted account state in `~/.pi/agent/codex-accounts.json`
|
|
110
|
+
|
|
58
111
|
## Project direction
|
|
59
112
|
|
|
60
113
|
This project is maintained as its own package and release line.
|
|
@@ -63,15 +116,19 @@ Current direction:
|
|
|
63
116
|
|
|
64
117
|
- package name: `@victor-software-house/pi-multicodex`
|
|
65
118
|
- Codex-only scope
|
|
66
|
-
- local state stored at `~/.pi/agent/codex-accounts.json`
|
|
67
|
-
-
|
|
119
|
+
- local account state stored at `~/.pi/agent/codex-accounts.json`
|
|
120
|
+
- footer and future extension settings stored under `pi-multicodex` in `~/.pi/agent/settings.json`
|
|
121
|
+
- internal logic split into focused modules today, with a broader shared controller planned next
|
|
68
122
|
- current roadmap tracked in `ROADMAP.md`
|
|
69
123
|
|
|
70
|
-
Current next
|
|
124
|
+
Current next milestones:
|
|
71
125
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
126
|
+
1. Replace the split command surface with the `/multicodex` command family.
|
|
127
|
+
2. Add dynamic autocomplete for subcommands and managed account identifiers.
|
|
128
|
+
3. Make account inspection and selection consistently actionable.
|
|
129
|
+
4. Persist footer settings immediately instead of waiting for panel close.
|
|
130
|
+
5. Add configurable rotation settings and document the rotation behavior contract.
|
|
131
|
+
6. Broaden the current footer controller into a shared MultiCodex controller.
|
|
75
132
|
|
|
76
133
|
## Behavior contract
|
|
77
134
|
|
|
@@ -96,7 +153,7 @@ The current runtime behavior is:
|
|
|
96
153
|
### Retry policy
|
|
97
154
|
|
|
98
155
|
- MultiCodex retries account rotation up to 5 times for a single request.
|
|
99
|
-
- Retries only happen for quota
|
|
156
|
+
- Retries only happen for quota and rate-limit style failures that occur before output is forwarded.
|
|
100
157
|
- Once output has started streaming, the original error is surfaced instead of rotating.
|
|
101
158
|
|
|
102
159
|
### Manual override behavior
|
|
@@ -132,38 +189,57 @@ pnpm check
|
|
|
132
189
|
npm pack --dry-run
|
|
133
190
|
```
|
|
134
191
|
|
|
135
|
-
Release
|
|
192
|
+
## Release process
|
|
193
|
+
|
|
194
|
+
This repository uses `semantic-release` with npm trusted publishing.
|
|
195
|
+
|
|
196
|
+
Maintainer flow:
|
|
197
|
+
|
|
198
|
+
1. Write Conventional Commits.
|
|
199
|
+
2. The local `commit-msg` hook validates commit messages with Lefthook + commitlint.
|
|
200
|
+
3. CI validates commit messages again and runs release checks.
|
|
201
|
+
4. Merge to `main`.
|
|
202
|
+
5. GitHub Actions runs `semantic-release` from `.github/workflows/publish.yml`.
|
|
203
|
+
6. `semantic-release` computes the next version, creates the git tag and GitHub release, updates `package.json` and `CHANGELOG.md`, and publishes to npm through trusted publishing.
|
|
136
204
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
205
|
+
Local verification:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
pnpm check
|
|
209
|
+
npm pack --dry-run
|
|
210
|
+
pnpm release:dry
|
|
211
|
+
```
|
|
141
212
|
|
|
142
213
|
Local push protection:
|
|
143
214
|
|
|
144
215
|
- `lefthook` runs `mise run pre-push`
|
|
145
|
-
- the `pre-push` mise task runs the same core validations as
|
|
216
|
+
- the `pre-push` mise task runs the same core validations as CI:
|
|
146
217
|
- `pnpm check`
|
|
147
218
|
- `npm pack --dry-run`
|
|
148
219
|
|
|
149
|
-
|
|
220
|
+
Do not use local `npm publish` for normal releases in this repo.
|
|
150
221
|
|
|
151
|
-
|
|
152
|
-
npm run release:prepare -- <version>
|
|
153
|
-
```
|
|
222
|
+
## npm trusted publishing setup
|
|
154
223
|
|
|
155
|
-
|
|
224
|
+
npm-side setup is required in addition to the workflow.
|
|
156
225
|
|
|
157
|
-
|
|
226
|
+
Trusted publisher mapping:
|
|
227
|
+
|
|
228
|
+
- package: `@victor-software-house/pi-multicodex`
|
|
229
|
+
- repository: `victor-founder/pi-multicodex`
|
|
230
|
+
- workflow file: `.github/workflows/publish.yml`
|
|
231
|
+
|
|
232
|
+
Useful commands:
|
|
158
233
|
|
|
159
234
|
```bash
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
git tag v<version>
|
|
163
|
-
git push origin main --tags
|
|
235
|
+
npm trust list @victor-software-house/pi-multicodex
|
|
236
|
+
script -q /dev/null bash -lc 'npm trust github @victor-software-house/pi-multicodex --repository victor-founder/pi-multicodex --file publish.yml --yes'
|
|
164
237
|
```
|
|
165
238
|
|
|
166
|
-
|
|
239
|
+
## Related docs
|
|
240
|
+
|
|
241
|
+
- `ROADMAP.md` for planned milestones and acceptance criteria
|
|
242
|
+
- `AGENTS.md` for repository-specific agent guidance
|
|
167
243
|
|
|
168
244
|
## Acknowledgment
|
|
169
245
|
|
package/account-manager.ts
CHANGED
|
@@ -195,6 +195,25 @@ export class AccountManager {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
removeAccount(email: string): boolean {
|
|
199
|
+
const index = this.data.accounts.findIndex(
|
|
200
|
+
(account) => account.email === email,
|
|
201
|
+
);
|
|
202
|
+
if (index < 0) return false;
|
|
203
|
+
|
|
204
|
+
this.data.accounts.splice(index, 1);
|
|
205
|
+
this.usageCache.delete(email);
|
|
206
|
+
if (this.manualEmail === email) {
|
|
207
|
+
this.manualEmail = undefined;
|
|
208
|
+
}
|
|
209
|
+
if (this.data.activeEmail === email) {
|
|
210
|
+
this.data.activeEmail = this.data.accounts[0]?.email;
|
|
211
|
+
}
|
|
212
|
+
this.save();
|
|
213
|
+
this.notifyStateChanged();
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
198
217
|
getCachedUsage(email: string): CodexUsageSnapshot | undefined {
|
|
199
218
|
return this.usageCache.get(email);
|
|
200
219
|
}
|
package/commands.ts
CHANGED
|
@@ -3,6 +3,14 @@ import type {
|
|
|
3
3
|
ExtensionAPI,
|
|
4
4
|
ExtensionCommandContext,
|
|
5
5
|
} from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { getSelectListTheme } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import {
|
|
8
|
+
Container,
|
|
9
|
+
Key,
|
|
10
|
+
matchesKey,
|
|
11
|
+
SelectList,
|
|
12
|
+
Text,
|
|
13
|
+
} from "@mariozechner/pi-tui";
|
|
6
14
|
import type { AccountManager } from "./account-manager";
|
|
7
15
|
import { openLoginInBrowser } from "./browser";
|
|
8
16
|
import type { createUsageStatusController } from "./status";
|
|
@@ -68,6 +76,109 @@ async function useOrLoginAccount(
|
|
|
68
76
|
await loginAndActivateAccount(pi, ctx, accountManager, identifier);
|
|
69
77
|
}
|
|
70
78
|
|
|
79
|
+
type AccountPanelResult =
|
|
80
|
+
| { action: "select"; email: string }
|
|
81
|
+
| { action: "remove"; email: string }
|
|
82
|
+
| undefined;
|
|
83
|
+
|
|
84
|
+
function getAccountLabel(email: string, quotaExhaustedUntil?: number): string {
|
|
85
|
+
if (!quotaExhaustedUntil || quotaExhaustedUntil <= Date.now()) {
|
|
86
|
+
return email;
|
|
87
|
+
}
|
|
88
|
+
return `${email} (Quota)`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function openAccountSelectionPanel(
|
|
92
|
+
ctx: ExtensionCommandContext,
|
|
93
|
+
accountManager: AccountManager,
|
|
94
|
+
): Promise<AccountPanelResult> {
|
|
95
|
+
const accounts = accountManager.getAccounts();
|
|
96
|
+
const items = accounts.map((account) => ({
|
|
97
|
+
value: account.email,
|
|
98
|
+
label: getAccountLabel(account.email, account.quotaExhaustedUntil),
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
return ctx.ui.custom<AccountPanelResult>((_tui, theme, _kb, done) => {
|
|
102
|
+
const container = new Container();
|
|
103
|
+
container.addChild(
|
|
104
|
+
new Text(theme.fg("accent", theme.bold("Select Account")), 1, 0),
|
|
105
|
+
);
|
|
106
|
+
container.addChild(
|
|
107
|
+
new Text(
|
|
108
|
+
theme.fg("dim", "Enter: use Backspace: remove account Esc: cancel"),
|
|
109
|
+
1,
|
|
110
|
+
0,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const selectList = new SelectList(items, 10, getSelectListTheme());
|
|
115
|
+
selectList.onSelect = (item) => {
|
|
116
|
+
done({ action: "select", email: item.value });
|
|
117
|
+
};
|
|
118
|
+
selectList.onCancel = () => done(undefined);
|
|
119
|
+
container.addChild(selectList);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
render: (width: number) => container.render(width),
|
|
123
|
+
invalidate: () => container.invalidate(),
|
|
124
|
+
handleInput: (data: string) => {
|
|
125
|
+
if (matchesKey(data, Key.backspace)) {
|
|
126
|
+
const selected = selectList.getSelectedItem();
|
|
127
|
+
if (selected) {
|
|
128
|
+
done({ action: "remove", email: selected.value });
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
selectList.handleInput(data);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function openAccountSelectionFlow(
|
|
139
|
+
ctx: ExtensionCommandContext,
|
|
140
|
+
accountManager: AccountManager,
|
|
141
|
+
statusController: ReturnType<typeof createUsageStatusController>,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
while (true) {
|
|
144
|
+
const accounts = accountManager.getAccounts();
|
|
145
|
+
if (accounts.length === 0) {
|
|
146
|
+
ctx.ui.notify(
|
|
147
|
+
"No managed accounts found. Use /login or /multicodex-use <identifier> first.",
|
|
148
|
+
"warning",
|
|
149
|
+
);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result = await openAccountSelectionPanel(ctx, accountManager);
|
|
154
|
+
if (!result) return;
|
|
155
|
+
|
|
156
|
+
if (result.action === "select") {
|
|
157
|
+
accountManager.setManualAccount(result.email);
|
|
158
|
+
ctx.ui.notify(`Now using ${result.email}`, "info");
|
|
159
|
+
await statusController.refreshFor(ctx);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const accountToRemove = accountManager.getAccount(result.email);
|
|
164
|
+
if (!accountToRemove) continue;
|
|
165
|
+
|
|
166
|
+
const active = accountManager.getActiveAccount();
|
|
167
|
+
const isActive = active?.email === result.email;
|
|
168
|
+
const message = isActive
|
|
169
|
+
? `Remove ${result.email}? This account is currently active and MultiCodex will switch to another account.`
|
|
170
|
+
: `Remove ${result.email}?`;
|
|
171
|
+
const confirmed = await ctx.ui.confirm("Remove account", message);
|
|
172
|
+
if (!confirmed) continue;
|
|
173
|
+
|
|
174
|
+
const removed = accountManager.removeAccount(result.email);
|
|
175
|
+
if (!removed) continue;
|
|
176
|
+
|
|
177
|
+
ctx.ui.notify(`Removed ${result.email}`, "info");
|
|
178
|
+
await statusController.refreshFor(ctx);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
71
182
|
export function registerCommands(
|
|
72
183
|
pi: ExtensionAPI,
|
|
73
184
|
accountManager: AccountManager,
|
|
@@ -88,30 +199,7 @@ export function registerCommands(
|
|
|
88
199
|
}
|
|
89
200
|
|
|
90
201
|
await accountManager.syncImportedOpenAICodexAuth();
|
|
91
|
-
|
|
92
|
-
if (accounts.length === 0) {
|
|
93
|
-
ctx.ui.notify(
|
|
94
|
-
"No managed accounts found. Use /login or /multicodex-use <identifier> first.",
|
|
95
|
-
"warning",
|
|
96
|
-
);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const options = accounts.map(
|
|
101
|
-
(account) =>
|
|
102
|
-
account.email +
|
|
103
|
-
(account.quotaExhaustedUntil &&
|
|
104
|
-
account.quotaExhaustedUntil > Date.now()
|
|
105
|
-
? " (Quota)"
|
|
106
|
-
: ""),
|
|
107
|
-
);
|
|
108
|
-
const selected = await ctx.ui.select("Select Account", options);
|
|
109
|
-
if (!selected) return;
|
|
110
|
-
|
|
111
|
-
const email = selected.split(" (")[0] ?? selected;
|
|
112
|
-
accountManager.setManualAccount(email);
|
|
113
|
-
ctx.ui.notify(`Now using ${email}`, "info");
|
|
114
|
-
await statusController.refreshFor(ctx);
|
|
202
|
+
await openAccountSelectionFlow(ctx, accountManager, statusController);
|
|
115
203
|
},
|
|
116
204
|
});
|
|
117
205
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@victor-software-house/pi-multicodex",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Codex account rotation extension for pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -56,8 +56,7 @@
|
|
|
56
56
|
"tsgo": "tsgo -p tsconfig.json",
|
|
57
57
|
"check": "pnpm lint && pnpm tsgo && pnpm test",
|
|
58
58
|
"pack:dry": "npm pack --dry-run",
|
|
59
|
-
"release:dry": "
|
|
60
|
-
"release:prepare": "bun ./scripts/publish.ts"
|
|
59
|
+
"release:dry": "pnpm exec semantic-release --dry-run"
|
|
61
60
|
},
|
|
62
61
|
"peerDependencies": {
|
|
63
62
|
"@mariozechner/pi-ai": "*",
|
|
@@ -77,11 +76,20 @@
|
|
|
77
76
|
},
|
|
78
77
|
"devDependencies": {
|
|
79
78
|
"@biomejs/biome": "^2.4.7",
|
|
79
|
+
"@commitlint/cli": "^20.4.4",
|
|
80
|
+
"@commitlint/config-conventional": "^20.4.4",
|
|
80
81
|
"@mariozechner/pi-ai": "^0.58.1",
|
|
81
82
|
"@mariozechner/pi-coding-agent": "^0.58.1",
|
|
82
83
|
"@mariozechner/pi-tui": "^0.58.1",
|
|
84
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
85
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
86
|
+
"@semantic-release/git": "^10.0.1",
|
|
87
|
+
"@semantic-release/github": "^12.0.6",
|
|
88
|
+
"@semantic-release/npm": "^13.1.5",
|
|
89
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
83
90
|
"@types/node": "^25.5.0",
|
|
84
91
|
"@typescript/native-preview": "7.0.0-dev.20260314.1",
|
|
92
|
+
"semantic-release": "^25.0.3",
|
|
85
93
|
"typescript": "^5.9.3",
|
|
86
94
|
"vitest": "^4.1.0"
|
|
87
95
|
},
|
package/status.ts
CHANGED
|
@@ -24,6 +24,7 @@ const REFRESH_INTERVAL_MS = 60_000;
|
|
|
24
24
|
const MODEL_SELECT_REFRESH_DEBOUNCE_MS = 250;
|
|
25
25
|
const UNKNOWN_PERCENT = "--";
|
|
26
26
|
const BRAND_LABEL = "Codex";
|
|
27
|
+
const SEGMENT_SEPARATOR = "·";
|
|
27
28
|
const FIVE_HOUR_LABEL = "5h:";
|
|
28
29
|
const SEVEN_DAY_LABEL = "7d:";
|
|
29
30
|
|
|
@@ -148,25 +149,40 @@ function formatLoading(ctx: ExtensionContext): string {
|
|
|
148
149
|
return ctx.ui.theme.fg("muted", "loading...");
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
function
|
|
152
|
-
ctx
|
|
152
|
+
function formatSeparator(ctx: ExtensionContext): string {
|
|
153
|
+
return ctx.ui.theme.fg("muted", SEGMENT_SEPARATOR);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getUsageSeverityToken(
|
|
153
157
|
displayPercent: number | undefined,
|
|
154
158
|
mode: PercentDisplayMode,
|
|
155
|
-
):
|
|
159
|
+
): "success" | "thinkingMedium" | "warning" | "error" | "dim" {
|
|
156
160
|
if (typeof displayPercent !== "number" || Number.isNaN(displayPercent)) {
|
|
157
|
-
return
|
|
161
|
+
return "dim";
|
|
158
162
|
}
|
|
159
163
|
|
|
160
|
-
const text = `${Math.round(clampPercent(displayPercent))}% ${mode}`;
|
|
161
164
|
if (mode === "left") {
|
|
162
|
-
if (displayPercent <= 10) return
|
|
163
|
-
if (displayPercent <= 25) return
|
|
164
|
-
return
|
|
165
|
+
if (displayPercent <= 10) return "error";
|
|
166
|
+
if (displayPercent <= 25) return "warning";
|
|
167
|
+
if (displayPercent <= 50) return "thinkingMedium";
|
|
168
|
+
return "success";
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
if (displayPercent >= 90) return
|
|
168
|
-
if (displayPercent >= 75) return
|
|
169
|
-
return
|
|
171
|
+
if (displayPercent >= 90) return "error";
|
|
172
|
+
if (displayPercent >= 75) return "warning";
|
|
173
|
+
if (displayPercent >= 50) return "thinkingMedium";
|
|
174
|
+
return "success";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function formatPercent(
|
|
178
|
+
displayPercent: number | undefined,
|
|
179
|
+
mode: PercentDisplayMode,
|
|
180
|
+
): string {
|
|
181
|
+
if (typeof displayPercent !== "number" || Number.isNaN(displayPercent)) {
|
|
182
|
+
return UNKNOWN_PERCENT;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return `${Math.round(clampPercent(displayPercent))}% ${mode}`;
|
|
170
186
|
}
|
|
171
187
|
|
|
172
188
|
function formatResetCountdown(resetAt: number | undefined): string | undefined {
|
|
@@ -200,20 +216,23 @@ function formatUsageSegment(
|
|
|
200
216
|
showReset: boolean,
|
|
201
217
|
preferences: FooterPreferences,
|
|
202
218
|
): string {
|
|
219
|
+
const displayPercent = usedToDisplayPercent(
|
|
220
|
+
usedPercent,
|
|
221
|
+
preferences.usageMode,
|
|
222
|
+
);
|
|
203
223
|
const parts = [
|
|
204
|
-
`${
|
|
205
|
-
ctx,
|
|
206
|
-
usedToDisplayPercent(usedPercent, preferences.usageMode),
|
|
207
|
-
preferences.usageMode,
|
|
208
|
-
)}`,
|
|
224
|
+
`${label}${formatPercent(displayPercent, preferences.usageMode)}`,
|
|
209
225
|
];
|
|
210
226
|
if (showReset) {
|
|
211
227
|
const countdown = formatResetCountdown(resetAt);
|
|
212
228
|
if (countdown) {
|
|
213
|
-
parts.push(
|
|
229
|
+
parts.push(`(↺${countdown})`);
|
|
214
230
|
}
|
|
215
231
|
}
|
|
216
|
-
return
|
|
232
|
+
return ctx.ui.theme.fg(
|
|
233
|
+
getUsageSeverityToken(displayPercent, preferences.usageMode),
|
|
234
|
+
parts.join(" "),
|
|
235
|
+
);
|
|
217
236
|
}
|
|
218
237
|
|
|
219
238
|
export function isManagedModel(model: MaybeModel): boolean {
|
|
@@ -227,7 +246,7 @@ export function formatActiveAccountStatus(
|
|
|
227
246
|
preferences: FooterPreferences,
|
|
228
247
|
): string {
|
|
229
248
|
const accountText = preferences.showAccount
|
|
230
|
-
? ctx.ui.theme.fg("
|
|
249
|
+
? ctx.ui.theme.fg("text", accountEmail)
|
|
231
250
|
: undefined;
|
|
232
251
|
if (!usage) {
|
|
233
252
|
return [formatBrand(ctx), accountText, formatLoading(ctx)]
|
|
@@ -252,16 +271,18 @@ export function formatActiveAccountStatus(
|
|
|
252
271
|
preferences,
|
|
253
272
|
);
|
|
254
273
|
|
|
274
|
+
const usageSegments = [fiveHour, sevenDay].filter(Boolean);
|
|
275
|
+
const usageText = usageSegments.join(` ${formatSeparator(ctx)} `);
|
|
255
276
|
const leading =
|
|
256
277
|
preferences.order === "account-first"
|
|
257
|
-
? [formatBrand(ctx), accountText]
|
|
258
|
-
: [formatBrand(ctx)];
|
|
278
|
+
? [formatBrand(ctx), accountText, usageText]
|
|
279
|
+
: [formatBrand(ctx), usageText];
|
|
259
280
|
const trailing =
|
|
260
281
|
preferences.order === "account-first" ? [] : [accountText].filter(Boolean);
|
|
261
282
|
|
|
262
|
-
return [...leading,
|
|
283
|
+
return [...leading, ...trailing]
|
|
263
284
|
.filter(Boolean)
|
|
264
|
-
.join(
|
|
285
|
+
.join(` ${formatSeparator(ctx)} `);
|
|
265
286
|
}
|
|
266
287
|
|
|
267
288
|
function getBooleanLabel(value: boolean): string {
|