@victor-software-house/pi-multicodex 1.0.9 → 1.0.11

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.
Files changed (3) hide show
  1. package/README.md +53 -5
  2. package/package.json +1 -1
  3. package/status.ts +55 -29
package/README.md CHANGED
@@ -69,11 +69,59 @@ Current direction:
69
69
 
70
70
  Current next step:
71
71
 
72
- - mirror the existing codex usage footer style, including support for displaying both reset countdowns
73
- - debounce expensive refresh work during rapid model cycling
74
- - move each reset countdown next to its matching usage period
75
- - add live preview to the `/multicodex-footer` panel before locking the final style
76
- - tighten footer updates so account switches and quota rotation are reflected immediately
72
+ - refine the footer color palette with small visual adjustments only
73
+ - document the account-rotation behavior contract explicitly
74
+ - improve the `/multicodex-use` and `/multicodex-status` everyday UX
75
+
76
+ ## Behavior contract
77
+
78
+ The current runtime behavior is:
79
+
80
+ ### Account selection priority
81
+
82
+ 1. Use the manual account selected with `/multicodex-use` when it is still available.
83
+ 2. Otherwise clear the stale manual override and select the best available managed account.
84
+ 3. Best-account selection prefers:
85
+ - untouched accounts with usage data
86
+ - then the account whose weekly reset window ends first
87
+ - then a random available account as fallback
88
+
89
+ ### Quota exhaustion semantics
90
+
91
+ - Quota and rate-limit style failures are detected from provider error text.
92
+ - When a request fails before any output is streamed, MultiCodex marks that account exhausted and retries on another account.
93
+ - Exhaustion lasts until the next known reset time.
94
+ - If usage data does not provide a reset time, exhaustion falls back to a 1 hour cooldown.
95
+
96
+ ### Retry policy
97
+
98
+ - MultiCodex retries account rotation up to 5 times for a single request.
99
+ - Retries only happen for quota/rate-limit style failures that occur before output is forwarded.
100
+ - Once output has started streaming, the original error is surfaced instead of rotating.
101
+
102
+ ### Manual override behavior
103
+
104
+ - `/multicodex-use <identifier>` sets the manual account override immediately.
105
+ - `/multicodex-use` with no argument opens the account picker and sets the selected manual override.
106
+ - Manual override is session-local state.
107
+ - Manual override clears automatically when the selected account is no longer available or when it hits quota during rotation.
108
+
109
+ ### Usage cache and refresh rules
110
+
111
+ - Usage is cached in memory for 5 minutes per account.
112
+ - Footer updates render cached usage immediately and refresh in the background when needed.
113
+ - Rapid `model_select` changes debounce background refresh work so non-Codex model switching clears the footer immediately.
114
+
115
+ ### Error classification
116
+
117
+ Quota rotation currently treats these error classes as interchangeable:
118
+
119
+ - HTTP `429`
120
+ - `quota`
121
+ - `usage limit`
122
+ - `rate limit`
123
+ - `too many requests`
124
+ - `limit reached`
77
125
 
78
126
  ## Release validation
79
127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victor-software-house/pi-multicodex",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Codex account rotation extension for pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/status.ts CHANGED
@@ -23,6 +23,8 @@ const SETTINGS_FILE = path.join(os.homedir(), ".pi", "agent", "settings.json");
23
23
  const REFRESH_INTERVAL_MS = 60_000;
24
24
  const MODEL_SELECT_REFRESH_DEBOUNCE_MS = 250;
25
25
  const UNKNOWN_PERCENT = "--";
26
+ const BRAND_LABEL = "Codex";
27
+ const SEGMENT_SEPARATOR = "·";
26
28
  const FIVE_HOUR_LABEL = "5h:";
27
29
  const SEVEN_DAY_LABEL = "7d:";
28
30
 
@@ -139,25 +141,48 @@ function usedToDisplayPercent(
139
141
  return mode === "left" ? left : clampPercent(100 - left);
140
142
  }
141
143
 
142
- function formatPercent(
143
- ctx: ExtensionContext,
144
+ function formatBrand(ctx: ExtensionContext): string {
145
+ return ctx.ui.theme.fg("muted", BRAND_LABEL);
146
+ }
147
+
148
+ function formatLoading(ctx: ExtensionContext): string {
149
+ return ctx.ui.theme.fg("muted", "loading...");
150
+ }
151
+
152
+ function formatSeparator(ctx: ExtensionContext): string {
153
+ return ctx.ui.theme.fg("muted", SEGMENT_SEPARATOR);
154
+ }
155
+
156
+ function getUsageSeverityToken(
144
157
  displayPercent: number | undefined,
145
158
  mode: PercentDisplayMode,
146
- ): string {
159
+ ): "success" | "thinkingMedium" | "warning" | "error" | "dim" {
147
160
  if (typeof displayPercent !== "number" || Number.isNaN(displayPercent)) {
148
- return ctx.ui.theme.fg("muted", UNKNOWN_PERCENT);
161
+ return "dim";
149
162
  }
150
163
 
151
- const text = `${Math.round(clampPercent(displayPercent))}% ${mode}`;
152
164
  if (mode === "left") {
153
- if (displayPercent <= 10) return ctx.ui.theme.fg("error", text);
154
- if (displayPercent <= 25) return ctx.ui.theme.fg("warning", text);
155
- return ctx.ui.theme.fg("success", text);
165
+ if (displayPercent <= 10) return "error";
166
+ if (displayPercent <= 25) return "warning";
167
+ if (displayPercent <= 50) return "thinkingMedium";
168
+ return "success";
156
169
  }
157
170
 
158
- if (displayPercent >= 90) return ctx.ui.theme.fg("error", text);
159
- if (displayPercent >= 75) return ctx.ui.theme.fg("warning", text);
160
- return ctx.ui.theme.fg("success", text);
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}`;
161
186
  }
162
187
 
163
188
  function formatResetCountdown(resetAt: number | undefined): string | undefined {
@@ -191,20 +216,23 @@ function formatUsageSegment(
191
216
  showReset: boolean,
192
217
  preferences: FooterPreferences,
193
218
  ): string {
219
+ const displayPercent = usedToDisplayPercent(
220
+ usedPercent,
221
+ preferences.usageMode,
222
+ );
194
223
  const parts = [
195
- `${ctx.ui.theme.fg("dim", label)}${formatPercent(
196
- ctx,
197
- usedToDisplayPercent(usedPercent, preferences.usageMode),
198
- preferences.usageMode,
199
- )}`,
224
+ `${label}${formatPercent(displayPercent, preferences.usageMode)}`,
200
225
  ];
201
226
  if (showReset) {
202
227
  const countdown = formatResetCountdown(resetAt);
203
228
  if (countdown) {
204
- parts.push(ctx.ui.theme.fg("dim", `(↺${countdown})`));
229
+ parts.push(`(↺${countdown})`);
205
230
  }
206
231
  }
207
- return parts.join(" ");
232
+ return ctx.ui.theme.fg(
233
+ getUsageSeverityToken(displayPercent, preferences.usageMode),
234
+ parts.join(" "),
235
+ );
208
236
  }
209
237
 
210
238
  export function isManagedModel(model: MaybeModel): boolean {
@@ -218,14 +246,10 @@ export function formatActiveAccountStatus(
218
246
  preferences: FooterPreferences,
219
247
  ): string {
220
248
  const accountText = preferences.showAccount
221
- ? ctx.ui.theme.fg("muted", accountEmail)
249
+ ? ctx.ui.theme.fg("text", accountEmail)
222
250
  : undefined;
223
251
  if (!usage) {
224
- return [
225
- ctx.ui.theme.fg("dim", "Codex"),
226
- accountText,
227
- ctx.ui.theme.fg("dim", "loading..."),
228
- ]
252
+ return [formatBrand(ctx), accountText, formatLoading(ctx)]
229
253
  .filter(Boolean)
230
254
  .join(" ");
231
255
  }
@@ -247,16 +271,18 @@ export function formatActiveAccountStatus(
247
271
  preferences,
248
272
  );
249
273
 
274
+ const usageSegments = [fiveHour, sevenDay].filter(Boolean);
275
+ const usageText = usageSegments.join(` ${formatSeparator(ctx)} `);
250
276
  const leading =
251
277
  preferences.order === "account-first"
252
- ? [ctx.ui.theme.fg("dim", "Codex"), accountText]
253
- : [ctx.ui.theme.fg("dim", "Codex")];
278
+ ? [formatBrand(ctx), accountText, usageText]
279
+ : [formatBrand(ctx), usageText];
254
280
  const trailing =
255
281
  preferences.order === "account-first" ? [] : [accountText].filter(Boolean);
256
282
 
257
- return [...leading, fiveHour, sevenDay, ...trailing]
283
+ return [...leading, ...trailing]
258
284
  .filter(Boolean)
259
- .join(" ");
285
+ .join(` ${formatSeparator(ctx)} `);
260
286
  }
261
287
 
262
288
  function getBooleanLabel(value: boolean): string {
@@ -498,7 +524,7 @@ export function createUsageStatusController(accountManager: AccountManager) {
498
524
  draft: FooterPreferences,
499
525
  ): string {
500
526
  const previewText =
501
- getStatusText(ctx, draft) ?? ctx.ui.theme.fg("dim", "Codex loading...");
527
+ getStatusText(ctx, draft) ?? `${formatBrand(ctx)} ${formatLoading(ctx)}`;
502
528
  return `${theme.fg("dim", "Preview")}: ${previewText}`;
503
529
  }
504
530