fix(codex): make controls and usage easier to navigate #5

Merged
eleith merged 2 commits from feat/codex-ux into main 2026-11-02 02:50:30 +00:00
26 changed files with 3293 additions and 1463 deletions

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@eleith-pi/codex": patch
---
Make Codex controls easier to use with matching context and fast on/off/toggle commands, a shared local dashboard, descriptive subcommand completion, clearer allowance reports, and a native cancellable loader for account usage requests.
@@ -0,0 +1,172 @@
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import {
CombinedAutocompleteProvider,
Editor,
type EditorTheme,
type Terminal,
TuiMainScreen,
} from "@earendil-works/pi-tui";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { completeCodexArguments } from "./commands.ts";
const identity = (text: string) => text;
const theme: EditorTheme = {
borderColor: identity,
selectList: {
selectedPrefix: identity,
selectedText: identity,
description: identity,
scrollInfo: identity,
noMatch: identity,
},
};
let emptyFileDirectory: string;
beforeEach(() => {
// File completion must see a real, empty directory, not the user's cwd/home.
const cache = fileURLToPath(new URL("../../../.cache/", import.meta.url));
mkdirSync(cache, { recursive: true });
emptyFileDirectory = mkdtempSync(join(cache, "codex-editor-empty-"));
vi.useFakeTimers();
});
afterEach(() => {
vi.clearAllTimers();
vi.useRealTimers();
vi.restoreAllMocks();
rmSync(emptyFileDirectory, { recursive: true, force: true });
});
it.each([
["fast", "Request priority processing", "Request standard processing"],
["context", "Use the larger local context window", "Use the catalog baseline context window"],
])(
"reopens %s controls on a typed prefix and replaces the whole argument",
async (group, onHelp, offHelp) => {
const { editor, getArgumentCompletions, getSuggestions } = createEditor();
await acceptCodexCommand(editor);
typeText(editor, group);
await vi.waitFor(
() => {
expect(getArgumentCompletions).toHaveBeenLastCalledWith(group);
expect(editor.isShowingAutocomplete()).toBe(true);
},
{ timeout: 1_000 },
);
editor.handleInput("\t");
expect(editor.getText()).toBe(`/eleith:codex ${group}`);
expect(editor.isShowingAutocomplete()).toBe(false);
const lookupsBeforeSpace = getSuggestions.mock.calls.length;
editor.handleInput(" ");
await vi.advanceTimersByTimeAsync(200);
expect(editor.getText()).toBe(`/eleith:codex ${group} `);
expect(editor.isShowingAutocomplete()).toBe(false);
expect(getSuggestions).toHaveBeenCalledTimes(lookupsBeforeSpace);
editor.handleInput("o");
await vi.waitFor(
() => {
expect(getArgumentCompletions).toHaveBeenLastCalledWith(`${group} o`);
expect(editor.isShowingAutocomplete()).toBe(true);
},
{ timeout: 1_000 },
);
// Editor exposes menu visibility, not its items. Observe the real provider's
// public result and the rendered list instead of inspecting editor internals.
const lookup = getSuggestions.mock.results.at(-1);
if (lookup?.type !== "return") throw new Error("Native suggestion lookup did not return");
const suggestions = await lookup.value;
expect(suggestions?.prefix).toBe(`${group} o`);
expect(suggestions?.items.map(({ value, label }) => ({ value, label }))).toEqual([
{ value: `${group} on`, label: "on" },
{ value: `${group} off`, label: "off" },
]);
const rendered = editor.render(120).join("\n");
expect(rendered).toContain(onHelp);
expect(rendered).toContain(offHelp);
editor.handleInput("\t");
expect(editor.getText()).toBe(`/eleith:codex ${group} on`);
expect(editor.isShowingAutocomplete()).toBe(false);
},
);
it("routes empty-prefix Tab through the native file gate, not Codex arguments", async () => {
const { editor, getArgumentCompletions, getSuggestions, fileCompletionGate } = createEditor();
await acceptCodexCommand(editor);
expect(getArgumentCompletions).not.toHaveBeenCalled();
getSuggestions.mockClear();
fileCompletionGate.mockClear();
// Tab takes the forced-file path. Its native gate rejects this empty argument
// before a file lookup or the command's argument completer can run.
editor.handleInput("\t");
expect(fileCompletionGate).toHaveBeenCalledExactlyOnceWith(
["/eleith:codex "],
0,
"/eleith:codex ".length,
);
expect(fileCompletionGate).toHaveReturnedWith(false);
expect(getSuggestions).not.toHaveBeenCalled();
expect(getArgumentCompletions).not.toHaveBeenCalled();
expect(editor.isShowingAutocomplete()).toBe(false);
expect(editor.getText()).toBe("/eleith:codex ");
});
function createEditor() {
// Real TUI without terminal lifecycle, stdin, output, or scheduled rendering.
const terminal: Terminal = {
columns: 120,
rows: 40,
kittyProtocolActive: false,
start: () => {
throw new Error("This UI-only test must not start a terminal");
},
stop: vi.fn(),
drainInput: async () => {},
write: vi.fn(),
moveBy: vi.fn(),
hideCursor: vi.fn(),
showCursor: vi.fn(),
clearLine: vi.fn(),
clearFromCursor: vi.fn(),
clearScreen: vi.fn(),
setTitle: vi.fn(),
setProgress: vi.fn(),
setProgramStatus: vi.fn(),
};
const tui = new TuiMainScreen(terminal);
vi.spyOn(tui, "requestRender").mockImplementation(() => {});
const getArgumentCompletions = vi.fn(completeCodexArguments);
const provider = new CombinedAutocompleteProvider(
[{ name: "eleith:codex", getArgumentCompletions }],
emptyFileDirectory,
);
// Call-through spy: suggestion routing and replacement remain native Pi logic.
const getSuggestions = vi.spyOn(provider, "getSuggestions");
const fileCompletionGate = vi.spyOn(provider, "shouldTriggerFileCompletion");
const editor = new Editor(tui, theme);
editor.setAutocompleteProvider(provider);
return { editor, getArgumentCompletions, getSuggestions, fileCompletionGate };
}
function typeText(editor: Editor, text: string) {
for (const character of text) editor.handleInput(character);
}
async function acceptCodexCommand(editor: Editor) {
typeText(editor, "/eleith:cod");
await vi.waitFor(() => expect(editor.isShowingAutocomplete()).toBe(true), {
timeout: 1_000,
});
editor.handleInput("\t");
// Native command-name completion already appends a space and closes the menu.
expect(editor.getText()).toBe("/eleith:codex ");
expect(editor.isShowingAutocomplete()).toBe(false);
}
+126
View File
@@ -0,0 +1,126 @@
import { expect, it } from "vitest";
import { CODEX_ACTIONS, completeCodexArguments } from "./commands.ts";
it("offers context, fast, and usage at the root", async () => {
expect(CODEX_ACTIONS).toEqual(["context", "fast", "usage"]);
expect(await completeCodexArguments("")).toEqual([
{
value: "context",
label: "context",
description: "Control the local context window",
},
{
value: "fast",
label: "fast",
description: "Control priority processing and allowance use",
},
{
value: "usage",
label: "usage",
description: "Show account usage and allowance limits",
},
]);
});
it.each([
[" CO", ["context"]],
["\tFa", ["fast"]],
["US", ["usage"]],
["context", ["context"]],
["fast", ["fast"]],
[" \t ", ["context", "fast", "usage"]],
])("completes normalized root prefix %j", async (prefix, values) => {
expect((await completeCodexArguments(prefix))?.map(({ value }) => value)).toEqual(values);
});
it("offers local context controls with full replacement values and plain descriptions", async () => {
expect(await completeCodexArguments("context ")).toEqual([
{
value: "context on",
label: "on",
description: "Use the larger local context window",
},
{
value: "context off",
label: "off",
description: "Use the catalog baseline context window",
},
{
value: "context toggle",
label: "toggle",
description: "Switch between the larger local window and catalog baseline",
},
]);
expect(await completeCodexArguments(" \tCONTEXT\t ")).toEqual(
await completeCodexArguments("context "),
);
});
it("offers fast controls with full replacement values and helpful descriptions", async () => {
expect(await completeCodexArguments("fast ")).toEqual([
{
value: "fast on",
label: "on",
description: "Request priority processing (uses more allowance)",
},
{
value: "fast off",
label: "off",
description: "Request standard processing instead of priority",
},
{
value: "fast toggle",
label: "toggle",
description: "Switch fast mode on or off",
},
]);
expect(await completeCodexArguments(" \tFAST\t ")).toEqual(
await completeCodexArguments("fast "),
);
});
it.each([
["context o", ["context on", "context off"]],
[" CONTEXT\tOf", ["context off"]],
["context t", ["context toggle"]],
["context on", ["context on"]],
["fast o", ["fast on", "fast off"]],
[" FAST\tOf", ["fast off"]],
["fast t", ["fast toggle"]],
["fast on", ["fast on"]],
])("completes only the matching control for %j", async (prefix, values) => {
expect((await completeCodexArguments(prefix))?.map(({ value }) => value)).toEqual(values);
});
it.each([
"unknown",
"extend",
"restore",
"status",
"reset",
"ex",
"re",
"fast unknown",
"context unknown",
"fa on",
"con on",
"extend ",
"restore o",
"usage fast",
"fast on ",
"fast off\t",
"fast toggle extra",
"fast status",
"fast reset",
"fast o ",
"context on ",
"context off\t",
"context toggle extra",
"context status",
"context reset",
"context extend",
"context restore",
"context o ",
])("does not offer aliases or invent further arguments for %j", async (prefix) => {
expect(await completeCodexArguments(prefix)).toBe(null);
});
+57
View File
@@ -0,0 +1,57 @@
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
type NativeCommand = Parameters<ExtensionAPI["registerCommand"]>[1];
export const CODEX_ACTIONS = ["context", "fast", "usage"] as const;
const ACTION_HELP: Record<(typeof CODEX_ACTIONS)[number], string> = {
context: "Control the local context window",
fast: "Control priority processing and allowance use",
usage: "Show account usage and allowance limits",
};
const CONTROL_HELP = {
context: [
{ action: "on", description: "Use the larger local context window" },
{ action: "off", description: "Use the catalog baseline context window" },
{
action: "toggle",
description: "Switch between the larger local window and catalog baseline",
},
],
fast: [
{ action: "on", description: "Request priority processing (uses more allowance)" },
{ action: "off", description: "Request standard processing instead of priority" },
{ action: "toggle", description: "Switch fast mode on or off" },
],
} as const;
export const completeCodexArguments: NonNullable<NativeCommand["getArgumentCompletions"]> = (
prefix,
) => {
const input = prefix.trimStart().toLowerCase();
const split = input.search(/\s/);
if (split < 0) {
const matches = CODEX_ACTIONS.filter((action) => action.startsWith(input));
return matches.length
? matches.map((action) => ({
value: action,
label: action,
description: ACTION_HELP[action],
}))
: null;
}
const group = input.slice(0, split);
if (group !== "context" && group !== "fast") return null;
const argument = input.slice(split).trimStart();
if (/\s/.test(argument)) return null;
const matches = CONTROL_HELP[group].filter(({ action }) => action.startsWith(argument));
return matches.length
? matches.map(({ action, description }) => ({
value: `${group} ${action}`,
label: action,
description,
}))
: null;
};
File diff suppressed because it is too large. Load diff
+32 -36
View File
@@ -2,7 +2,7 @@ import type { Api, Model } from "@earendil-works/pi-ai";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { contextProfile, modelKey } from "./profiles.ts";
import { CONTEXT_ENTRY, readPreferences, type ContextPreference } from "./state.ts";
import { formatContextStatus } from "./status.ts";
import type { ContextStatus } from "./status.ts";
// Small public ports: no provider calls, catalog refresh, or settings-file reads.
export type ContextAPI = Pick<
@@ -92,20 +92,20 @@ export class ContextController {
async handle(args: string, ctx: ContextHost): Promise<void> {
if (this.disposed) return;
const action = args.trim().toLowerCase();
if (!action) {
this.status(ctx);
return;
}
if (action !== "extend" && action !== "restore") {
ctx.ui.notify(`Usage: /${this.commandName} [extend|restore]`, "info");
if (action !== "on" && action !== "off" && action !== "toggle") {
ctx.ui.notify(`Usage: /${this.commandName} context [on|off|toggle]`, "warning");
return;
}
if (this.changing || this.selecting || !ctx.isIdle()) {
ctx.ui.notify("Wait for the current run or context change to finish.", "warning");
return;
}
const model = ctx.model;
const extended =
action === "on" ||
(action === "toggle" && (!model || this.preferences.get(modelKey(model)) !== true));
this.replayPaused = false;
await this.change(ctx, action === "extend", true);
await this.change(ctx, extended, true);
}
shutdown(ctx?: ContextHost): void {
@@ -115,6 +115,27 @@ export class ContextController {
this.preferences.clear();
}
getStatus(ctx: ContextHost): ContextStatus | undefined {
if (this.disposed) return undefined;
const model = ctx.model;
if (!model) return undefined;
const profile = contextProfile(model);
const standardWindow = baseline(ctx, model);
let extendedWindow: number | undefined;
if (profile && standardWindow !== undefined) {
extendedWindow = Math.max(standardWindow, profile.extendedWindow);
}
return {
model,
usage: ctx.getContextUsage(),
standardWindow,
extendedWindow,
desiredExtended: this.preferences.get(modelKey(model)),
replayPaused: this.replayPaused,
branch: ctx.sessionManager.getBranch(),
};
}
private async change(ctx: ContextHost, extended: boolean, command: boolean): Promise<void> {
const current = ctx.model;
const profile = contextProfile(current);
@@ -131,7 +152,7 @@ export class ContextController {
const standardWindow = baseline(ctx, current);
if (standardWindow === undefined) {
ctx.ui.notify(
"Catalog baseline is unknown or incompatible. Reselect a catalog model and inspect context status before changing the window.",
`Catalog baseline is unknown or incompatible. Reselect a catalog model and inspect /${this.commandName} before changing the window.`,
"warning",
);
return;
@@ -189,31 +210,6 @@ export class ContextController {
this.preferences.set(key, extended);
}
private status(ctx: ContextHost): void {
const model = ctx.model;
if (!model) {
ctx.ui.notify("No model selected.", "info");
return;
}
const profile = contextProfile(model);
const standardWindow = baseline(ctx, model);
ctx.ui.notify(
formatContextStatus({
model,
usage: ctx.getContextUsage(),
standardWindow,
extendedWindow:
profile && standardWindow !== undefined
? Math.max(standardWindow, profile.extendedWindow)
: undefined,
desiredExtended: this.preferences.get(modelKey(model)),
replayPaused: this.replayPaused,
branch: ctx.sessionManager.getBranch(),
}),
"info",
);
}
private usageAllowsReduction(ctx: ContextHost, target: number): boolean {
const usage = ctx.getContextUsage()?.tokens;
if (typeof usage === "number" && Number.isFinite(usage) && usage > target) {
@@ -288,7 +284,7 @@ export class ContextController {
if (!accepted) {
this.replayPaused = true;
ctx.ui.notify(
"Could not change context: provider authentication is unavailable. Automatic replay is paused; explicitly extend or restore after checking authentication.",
`Could not change context: provider authentication is unavailable. Automatic replay is paused; explicitly use /${this.commandName} context on or /${this.commandName} context off after checking authentication.`,
"warning",
);
return false;
@@ -333,7 +329,7 @@ export class ContextController {
private uncertain(ctx: ContextHost): void {
this.replayPaused = true;
ctx.ui.notify(
`Context change interrupted or uncertain. In-flight selection cannot be cancelled and may replace your intended model. Inspect /${this.commandName}, reselect the intended model if needed, then explicitly extend or restore. Automatic replay is paused; try /${this.commandName} again when ready.`,
`Context change interrupted or uncertain. In-flight selection cannot be cancelled and may replace your intended model. Inspect /${this.commandName}, reselect the intended model if needed, then explicitly use /${this.commandName} context on or /${this.commandName} context off. Automatic replay is paused.`,
"warning",
);
}
@@ -0,0 +1,127 @@
import { vi } from "vitest";
import type {
BorderedLoader,
ExtensionUIContext,
KeybindingsManager,
Theme,
} from "@earendil-works/pi-coding-agent";
type Tui = ConstructorParameters<typeof BorderedLoader>[0];
type UIComponent = Awaited<ReturnType<Parameters<ExtensionUIContext["custom"]>[0]>>;
type CustomFactory<T> = (
tui: Tui,
theme: Theme,
kb: KeybindingsManager,
done: (result: T) => void,
) => UIComponent | Promise<UIComponent>;
// The public custom-interaction lifecycle, without starting a terminal. Model Pi 1.3's
// shared editor restoration (not 1.4's dialog stack), so unsafe non-overlay closes
// really can steal focus from a newer interaction. Overlay removal owns only its entry.
export function customInteractionUI() {
const mainEditor: UIComponent = { render: () => ["editor"], invalidate() {} };
let editorComponent = mainEditor;
let focusedComponent = mainEditor;
const overlays: { component: UIComponent; preFocus: UIComponent }[] = [];
const requestRender = vi.fn();
const modals: {
component?: UIComponent;
lines: string[];
closed: boolean;
adopted: boolean;
close: ReturnType<typeof vi.fn<(result?: unknown) => void>>;
requestRender: ReturnType<typeof vi.fn<() => void>>;
}[] = [];
const restoreEditor = () => {
editorComponent = mainEditor;
focusedComponent = mainEditor;
requestRender();
};
const removeOwnedOverlay = (overlay: (typeof overlays)[number] | undefined) => {
if (!overlay) return;
const index = overlays.indexOf(overlay);
if (index === -1) return;
for (const entry of overlays) {
if (entry.preFocus === overlay.component) entry.preFocus = overlay.preFocus;
}
overlays.splice(index, 1);
if (focusedComponent === overlay.component) {
focusedComponent = overlays.at(-1)?.component ?? overlay.preFocus;
}
requestRender();
};
const custom = vi.fn<ExtensionUIContext["custom"]>(
<T>(factory: CustomFactory<T>, options?: Parameters<ExtensionUIContext["custom"]>[1]) =>
new Promise<T>((resolve, reject) => {
let overlay: (typeof overlays)[number] | undefined;
const modal = {
component: undefined as UIComponent | undefined,
lines: [] as string[],
closed: false,
adopted: false,
close: vi.fn(),
// Attribute animation requests to the factory while sharing the same UI surface.
requestRender: vi.fn(() => requestRender()),
};
modal.close.mockImplementation((result: T) => {
if (modal.closed) return;
modal.closed = true;
if (options?.overlay) removeOwnedOverlay(overlay);
else restoreEditor();
resolve(result);
if (modal.adopted) modal.component?.dispose?.();
});
modals.push(modal);
Promise.resolve(
factory(
{ requestRender: modal.requestRender } as unknown as Tui,
{ fg: (_color: string, text: string) => text } as Theme,
{} as KeybindingsManager,
modal.close,
),
)
.then((component) => {
// Keep the result observable even on early done, but never adopt or dispose it:
// native Pi leaves early-done cleanup to the factory's owner.
modal.component = component;
if (modal.closed) return;
modal.adopted = true;
if (options?.overlay) {
overlay = { component, preFocus: focusedComponent };
overlays.push(overlay);
} else {
editorComponent = component;
}
focusedComponent = component;
modal.lines = component.render(80);
requestRender();
})
.catch((error: unknown) => {
if (modal.closed) return;
if (!options?.overlay) restoreEditor();
reject(error);
});
}),
);
return {
// Vitest erases the generic call signature; the implementation preserves it.
custom: custom as ExtensionUIContext["custom"] & typeof custom,
modals,
requestRender,
mainEditor,
get editorComponent() {
return editorComponent;
},
get focusedComponent() {
return focusedComponent;
},
get components() {
return [editorComponent, ...overlays.map((entry) => entry.component)];
},
dispose() {
for (const modal of modals) modal.component?.dispose?.();
},
};
}
+97
View File
@@ -0,0 +1,97 @@
import type { Api, Model } from "@earendil-works/pi-ai";
import { expect, it } from "vitest";
import { formatCodexDashboard } from "./dashboard.ts";
import type { ContextStatus } from "./status.ts";
import type { FastStatus } from "./fast-status.ts";
const model: Model<Api> = {
id: "gpt-6-sol",
name: "Sol",
provider: "openai-codex",
api: "openai-codex-responses",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 272_000,
maxTokens: 128_000,
};
const context: ContextStatus = {
model,
usage: { tokens: 40_000, contextWindow: 272_000, percent: 14.7 },
standardWindow: 272_000,
extendedWindow: 922_000,
branch: [],
};
const inherited: FastStatus = {
preference: "inherit",
eligibility: "unknown",
requestPolicy: "untouched",
lastObserved: undefined,
conflict: false,
};
it("puts fast and context state together, with discoverable configured commands", () => {
const original = structuredClone({ context, inherited });
const text = formatCodexDashboard(context, inherited, "personal:codex");
expect(text).toContain("Codex · openai-codex/gpt-6-sol");
expect(text).toContain("Fast: Provider default (no override)");
expect(text).toContain("Context: 40k / 272k tokens (standard)");
expect(text).toContain("/personal:codex usage — account allowance");
expect(text).toContain(
'/personal:codex context on|off|toggle — local context window (type "context o" for on/off suggestions)',
);
expect(text).toContain(
'/personal:codex fast on|off|toggle — priority controls (type "fast o" for on/off suggestions)',
);
for (const removed of [
"extend|restore",
" fast status",
" fast reset",
" context status",
" context reset",
]) {
expect(text).not.toContain(removed);
}
expect(text).not.toContain("/eleith:codex");
expect(text).not.toContain("Eligibility:");
expect(text).not.toContain("Tab for options");
expect({ context, inherited }).toEqual(original);
});
it("does not present requested priority as confirmed server processing", () => {
const fast: FastStatus = {
...inherited,
preference: "on",
eligibility: "supported",
requestPolicy: "priority",
lastObserved: "default",
};
const text = formatCodexDashboard(context, fast, "eleith:codex");
expect(text).toContain("Fast: On (priority requested)\nLast reported tier: Standard");
expect(text).not.toContain("Last reported tier: Priority");
expect(text).toContain("Context: 40k / 272k tokens (standard)");
});
it("keeps the effective context window distinct from the selected model window", () => {
const snapshot: ContextStatus = {
...context,
model: { ...model, contextWindow: 922_000 },
};
const original = structuredClone(snapshot);
const text = formatCodexDashboard(snapshot, inherited, "eleith:codex");
expect(text).toContain(
"Fast: Provider default (no override)\nContext: 40k / 272k tokens (effective) · Selected context window: 922k tokens (extended)",
);
expect(text).not.toContain("Context: 40k / 922k");
expect(snapshot).toEqual(original);
});
it("keeps the context snapshot visible when fast state is unavailable", () => {
const text = formatCodexDashboard(context, undefined, "eleith:codex");
expect(text).toContain("Fast: unavailable\nContext: 40k / 272k tokens (standard)");
});
it("does not invent a model or context window when no model is selected", () => {
expect(formatCodexDashboard(undefined, undefined, "eleith:codex")).toBe("No model selected.");
});
+24
View File
@@ -0,0 +1,24 @@
import { formatContextSummary, type ContextStatus } from "./status.ts";
import { formatFastStatus, type FastStatus } from "./fast-status.ts";
import { modelKey } from "./profiles.ts";
/** Local snapshots only: opening the dashboard never fetches account data. */
export function formatCodexDashboard(
context: ContextStatus | undefined,
fast: FastStatus | undefined,
commandName: string,
now = Date.now(),
): string {
if (!context) return "No model selected.";
let fastSummary = "Fast: unavailable";
if (fast) fastSummary = formatFastStatus(fast);
return [
`Codex · ${modelKey(context.model)}`,
fastSummary,
formatContextSummary(context, now),
"",
`/${commandName} usage — account allowance`,
`/${commandName} context on|off|toggle — local context window (type "context o" for on/off suggestions)`,
`/${commandName} fast on|off|toggle — priority controls (type "fast o" for on/off suggestions)`,
].join("\n");
}
+111 -38
View File
@@ -54,11 +54,27 @@ it.each(["sse", "websocket"] as const)(
await openRealExtensionSession(runtime, model);
try {
const branch = structuredClone(manager.getBranch());
await session.prompt("/eleith:codex fast");
await session.prompt("/eleith:codex fast status");
expect(network.fetch).not.toHaveBeenCalled();
expect(manager.getBranch()).toEqual(branch);
expect(modelTurns).not.toHaveBeenCalled();
resolveAuth.mockClear();
let dashboard: string | undefined;
for (const command of ["/eleith:codex", "/eleith:codex fast", "/eleith:codex context"]) {
const notifications = notify.mock.calls.length;
await session.prompt(command);
expect(notify).toHaveBeenCalledTimes(notifications + 1);
expect(notify).toHaveBeenLastCalledWith(expect.stringContaining("Context:"), "info");
expect(notify).toHaveBeenLastCalledWith(
expect.stringContaining("Fast: Provider default (no override)"),
"info",
);
const message = notify.mock.calls.at(-1)![0] as string;
if (dashboard === undefined) dashboard = message;
else expect(message).toBe(dashboard);
expect(network.fetch).not.toHaveBeenCalled();
expect(resolveAuth).not.toHaveBeenCalled();
expect(credentialWrites).not.toHaveBeenCalled();
expect(manager.getBranch()).toEqual(branch);
expect(modelTurns).not.toHaveBeenCalled();
expect(confirm).not.toHaveBeenCalled();
}
await session.prompt("/eleith:codex fast on");
expect(confirm).toHaveBeenCalledOnce();
@@ -71,6 +87,10 @@ it.each(["sse", "websocket"] as const)(
{ provider: "openai-codex", modelId: model.id, mode: "on" },
]);
expect(modelTurns).not.toHaveBeenCalled();
expect(notify).toHaveBeenLastCalledWith(
expect.stringContaining("Fast: On (priority requested)\nNo response tier reported yet."),
"info",
);
resolveAuth.mockClear();
await session.prompt("Reply with ok.");
@@ -92,21 +112,39 @@ it.each(["sse", "websocket"] as const)(
const requests = network.fetch.mock.calls.length;
resolveAuth.mockClear();
const afterTurn = structuredClone(manager.getBranch());
for (const command of ["/eleith:codex fast status", "/eleith:codex fast"]) {
dashboard = undefined;
for (const command of ["/eleith:codex", "/eleith:codex fast", "/eleith:codex context"]) {
const notifications = notify.mock.calls.length;
await session.prompt(command);
expect(notify).toHaveBeenLastCalledWith(expect.stringContaining("Preference: on"), "info");
expect(notify).toHaveBeenCalledTimes(notifications + 1);
expect(notify).toHaveBeenLastCalledWith(
expect.stringContaining("Last requested tier: priority"),
"info",
);
expect(notify).toHaveBeenLastCalledWith(
expect.stringContaining("Last observed tier: default"),
expect.stringContaining("Fast: On (priority requested)\nLast reported tier: Standard"),
"info",
);
expect(notify).toHaveBeenLastCalledWith(expect.stringContaining("Context:"), "info");
const message = notify.mock.calls.at(-1)![0] as string;
if (dashboard === undefined) dashboard = message;
else expect(message).toBe(dashboard);
expect(network.fetch).toHaveBeenCalledTimes(requests);
expect(resolveAuth).not.toHaveBeenCalled();
expect(manager.getBranch()).toEqual(afterTurn);
expect(modelTurns).toHaveBeenCalledOnce();
expect(credentialWrites).not.toHaveBeenCalled();
expect(confirm).toHaveBeenCalledOnce();
}
await session.prompt("/eleith:codex fast toggle");
expect(notify).toHaveBeenLastCalledWith(
expect.stringContaining("Fast: Off (standard requested)"),
"info",
);
expect(fastChoices(manager)).toEqual([
{ provider: "openai-codex", modelId: model.id, mode: "on" },
{ provider: "openai-codex", modelId: model.id, mode: "off" },
]);
expect(confirm).toHaveBeenCalledOnce();
expect(network.fetch).toHaveBeenCalledTimes(requests);
expect(resolveAuth).not.toHaveBeenCalled();
expect(manager.getBranch()).toEqual(afterTurn);
expect(modelTurns).toHaveBeenCalledOnce();
expect(credentialWrites).not.toHaveBeenCalled();
expect(errors).toEqual([]);
@@ -116,12 +154,10 @@ it.each(["sse", "websocket"] as const)(
},
);
it("SDK replays the active fast branch alongside context controls, then off and reset change the wire", async () => {
it("SDK replays the active fast branch alongside context controls, explicit off and legacy inherit", async () => {
const network = installResponseTransports("sse");
const { runtime, model, credentialWrites, modelTurns } = await createOfflineModelRuntime(
"sse",
network.fetch,
);
const { runtime, model, credentialWrites, resolveAuth, modelTurns } =
await createOfflineModelRuntime("sse", network.fetch);
const manager = SessionManager.inMemory(directory);
const saved = { provider: "openai-codex", modelId: model.id, mode: "on" };
const onLeaf = manager.appendCustomEntry(FAST_ENTRY, saved);
@@ -133,19 +169,27 @@ it("SDK replays the active fast branch alongside context controls, then off and
model,
manager,
);
let extendedWindow: number | undefined;
const outputLimit = session.model?.maxTokens;
const thinking = session.thinkingLevel;
try {
expect(network.fetch).not.toHaveBeenCalled();
const branch = structuredClone(manager.getBranch());
resolveAuth.mockClear();
await session.prompt("/eleith:codex fast");
await session.prompt("/eleith:codex fast status");
expect(notify).toHaveBeenLastCalledWith(expect.stringContaining("Preference: on"), "info");
expect(notify).toHaveBeenLastCalledWith(
expect.stringContaining("Fast: Paused — model eligibility needs checking"),
"info",
);
expect(notify).toHaveBeenLastCalledWith(expect.stringContaining("Context:"), "info");
expect(network.fetch).not.toHaveBeenCalled();
expect(resolveAuth).not.toHaveBeenCalled();
expect(credentialWrites).not.toHaveBeenCalled();
expect(manager.getBranch()).toEqual(branch);
expect(modelTurns).not.toHaveBeenCalled();
const outputLimit = session.model?.maxTokens;
const thinking = session.thinkingLevel;
await session.prompt("/eleith:codex extend");
const extendedWindow = session.model?.contextWindow;
await session.prompt("/eleith:codex context on");
extendedWindow = session.model?.contextWindow;
expect(extendedWindow).toBeGreaterThanOrEqual(model.contextWindow);
expect(session.model?.maxTokens).toBe(outputLimit);
expect(session.thinkingLevel).toBe(thinking);
@@ -159,23 +203,13 @@ it("SDK replays the active fast branch alongside context controls, then off and
expect(confirm).not.toHaveBeenCalled();
await session.prompt("/eleith:codex fast off");
expect(notify).toHaveBeenLastCalledWith(
expect.stringContaining("Fast: Off (standard requested)"),
"info",
);
expect(fastChoices(manager)).toEqual([saved, { ...saved, mode: "off" }]);
await session.prompt("Reply with ok without priority.");
expect(network.payloads[1]).toMatchObject({ model: model.id, service_tier: "default" });
await session.prompt("/eleith:codex fast reset");
expect(fastChoices(manager)).toEqual([
saved,
{ ...saved, mode: "off" },
{ ...saved, mode: "inherit" },
]);
await session.prompt("Reply with ok using the provider default.");
expect(network.payloads[2]).toHaveProperty("model", model.id);
expect(network.payloads[2]).not.toHaveProperty("service_tier");
expect(network.payloads).toHaveLength(3);
expect(session.getLastAssistantText()).toBe("ok");
expect(modelTurns).toHaveBeenCalledTimes(3);
expect(network.catalog).toHaveBeenCalledOnce();
expect(session.model?.contextWindow).toBe(extendedWindow);
expect(session.model?.maxTokens).toBe(outputLimit);
expect(session.thinkingLevel).toBe(thinking);
@@ -184,6 +218,45 @@ it("SDK replays the active fast branch alongside context controls, then off and
} finally {
await closeSession();
}
// Old sessions may contain inherit entries. Restore that schema, not a reset command.
const inheritLeaf = manager.appendCustomEntry(FAST_ENTRY, { ...saved, mode: "inherit" });
manager.appendCustomEntry(FAST_ENTRY, saved);
manager.branch(inheritLeaf);
const legacy = await openRealExtensionSession(runtime, model, manager);
try {
const branch = structuredClone(manager.getBranch());
const requests = network.fetch.mock.calls.length;
resolveAuth.mockClear();
await legacy.session.prompt("/eleith:codex fast");
expect(legacy.notify).toHaveBeenLastCalledWith(
expect.stringContaining("Fast: Provider default (no override)"),
"info",
);
expect(fastChoices(manager)).toEqual([
saved,
{ ...saved, mode: "off" },
{ ...saved, mode: "inherit" },
]);
expect(manager.getBranch()).toEqual(branch);
expect(network.fetch).toHaveBeenCalledTimes(requests);
expect(resolveAuth).not.toHaveBeenCalled();
await legacy.session.prompt("Reply with ok using the provider default.");
expect(network.payloads[2]).toHaveProperty("model", model.id);
expect(network.payloads[2]).not.toHaveProperty("service_tier");
expect(network.payloads).toHaveLength(3);
expect(legacy.session.getLastAssistantText()).toBe("ok");
expect(modelTurns).toHaveBeenCalledTimes(3);
expect(network.catalog).toHaveBeenCalledOnce();
expect(legacy.session.model?.contextWindow).toBe(extendedWindow);
expect(legacy.session.model?.maxTokens).toBe(outputLimit);
expect(legacy.session.thinkingLevel).toBe(thinking);
expect(legacy.confirm).not.toHaveBeenCalled();
expect(credentialWrites).not.toHaveBeenCalled();
expect(legacy.errors).toEqual([]);
} finally {
await legacy.closeSession();
}
});
// Network policy: globals start closed; the supplied fetch accepts only these two fixed URLs.
+109 -31
View File
@@ -1,42 +1,120 @@
import { expect, it } from "vitest";
import { formatFastStatus, type FastStatus } from "./fast-status.ts";
import { formatFastStatus, type FastStatus, type ObservedTier } from "./fast-status.ts";
const suspended: FastStatus = {
preference: "on",
eligibility: "unknown",
requestPolicy: "suspended",
lastRequested: undefined,
lastObserved: undefined,
conflict: false,
};
it("leaves provider default brief without unknown fields, commands or cost warnings", () => {
const fast = createFastStatusFixture();
expect(formatFastStatus(fast)).toBe("Fast: Provider default (no override)");
});
it("labels unknown delivery and explains selected scope, allowance consumption and cost-estimate limits", () => {
const text = formatFastStatus(suspended);
expect(text.split("\n")).toEqual([
"Codex fast mode (selected model)",
"Preference: on",
"Eligibility: unknown",
"Next request policy: suspended",
"Last requested tier: unknown",
"Last observed tier: unknown",
"Scope: direct legacy Codex; may include retries, compaction and cache warming. Selected scope is not physical-request identity.",
"Priority uses more allowance. Pi's cost estimate may miss priority pricing when response tier metadata is absent or normalized.",
"Observed tier is model-scoped, not a receipt for the last requested tier.",
it("describes off as a standard request, not an observed processing tier", () => {
const fast = createFastStatusFixture({ preference: "off", requestPolicy: "default" });
expect(formatFastStatus(fast)).toBe("Fast: Off (standard requested)");
});
it("describes on as a priority request without claiming priority was delivered", () => {
const fast = createFastStatusFixture({
preference: "on",
eligibility: "supported",
requestPolicy: "priority",
});
expect(formatFastStatus(fast).split("\n")).toEqual([
"Fast: On (priority requested)",
"No response tier reported yet.",
"Priority requests use more allowance.",
]);
});
it("keeps requested and observed tiers distinct and adds the conflict note only when needed", () => {
const status: FastStatus = {
...suspended,
it("keeps next-request priority separate from a reported standard tier", () => {
const fast = createFastStatusFixture({
preference: "on",
eligibility: "supported",
requestPolicy: "priority",
lastRequested: "priority",
lastObserved: "default",
};
const text = formatFastStatus(status);
expect(text).toContain("Last requested tier: priority\nLast observed tier: default");
expect(text).not.toContain("Tier conflict:");
expect(formatFastStatus({ ...status, conflict: true })).toContain(
"Tier conflict: an existing non-owned tier was left untouched.",
});
expect(formatFastStatus(fast).split("\n")).toEqual([
"Fast: On (priority requested)",
"Last reported tier: Standard",
"Priority requests use more allowance.",
]);
});
it.each<[ObservedTier, string]>([
["default", "Standard"],
["priority", "Priority"],
["flex", "Flex"],
["auto", "Automatic"],
["fast", "Fast"],
])("labels the known reported tier %s as %s even without an override", (tier, label) => {
const fast = createFastStatusFixture({ lastObserved: tier });
expect(formatFastStatus(fast)).toBe(
`Fast: Provider default (no override)\nLast reported tier: ${label}`,
);
});
it("pauses a saved on choice when eligibility needs checking without claiming a current request", () => {
const fast = createFastStatusFixture({
preference: "on",
requestPolicy: "suspended",
});
expect(formatFastStatus(fast)).toBe("Fast: Paused — model eligibility needs checking");
});
it("explains when priority is not available without erasing the saved preference", () => {
const fast = createFastStatusFixture({
preference: "on",
eligibility: "unsupported",
requestPolicy: "suspended",
lastObserved: "default",
});
expect(formatFastStatus(fast)).toBe(
"Fast: Paused — priority is not available for this model\nLast reported tier: Standard",
);
expect(fast.preference).toBe("on");
});
it("gives outside scope precedence over the saved on preference and supported eligibility", () => {
const fast = createFastStatusFixture({
preference: "on",
eligibility: "supported",
requestPolicy: "outside scope",
});
expect(formatFastStatus(fast)).toBe("Fast: Unavailable for this model/provider");
});
it.each(["priority", "default"] as const)(
"reports a past conflict separately without suspending the next %s request",
(requestPolicy) => {
const fast = createFastStatusFixture({
preference: requestPolicy === "priority" ? "on" : "off",
eligibility: "supported",
requestPolicy,
lastObserved: "flex",
conflict: true,
});
const headline =
requestPolicy === "priority"
? "Fast: On (priority requested)"
: "Fast: Off (standard requested)";
const rows = [
headline,
"Last reported tier: Flex",
"Last request kept another tier setting; not overridden.",
];
if (requestPolicy === "priority") rows.push("Priority requests use more allowance.");
expect(formatFastStatus(fast).split("\n")).toEqual(rows);
expect(formatFastStatus({ ...fast, conflict: false })).not.toContain(
"Last request kept another tier setting",
);
},
);
function createFastStatusFixture(overrides: Partial<FastStatus> = {}): FastStatus {
return {
preference: "inherit",
eligibility: "unknown",
requestPolicy: "untouched",
lastObserved: undefined,
conflict: false,
...overrides,
};
}
+36 -15
View File
@@ -9,25 +9,46 @@ export interface FastStatus {
preference: FastPreference;
eligibility: FastEligibility;
requestPolicy: FastRequestPolicy;
lastRequested: RequestedTier | undefined;
lastObserved: ObservedTier | undefined;
conflict: boolean;
}
export function formatFastStatus(status: FastStatus): string {
const rows = [
"Codex fast mode (selected model)",
`Preference: ${status.preference}`,
`Eligibility: ${status.eligibility}`,
`Next request policy: ${status.requestPolicy}`,
`Last requested tier: ${status.lastRequested ?? "unknown"}`,
`Last observed tier: ${status.lastObserved ?? "unknown"}`,
];
if (status.conflict) rows.push("Tier conflict: an existing non-owned tier was left untouched.");
rows.push(
"Scope: direct legacy Codex; may include retries, compaction and cache warming. Selected scope is not physical-request identity.",
"Priority uses more allowance. Pi's cost estimate may miss priority pricing when response tier metadata is absent or normalized.",
"Observed tier is model-scoped, not a receipt for the last requested tier.",
);
let headline: string;
switch (status.requestPolicy) {
case "untouched":
headline = "Fast: Provider default (no override)";
break;
case "default":
headline = "Fast: Off (standard requested)";
break;
case "priority":
headline = "Fast: On (priority requested)";
break;
case "suspended":
headline =
status.eligibility === "unsupported"
? "Fast: Paused — priority is not available for this model"
: "Fast: Paused — model eligibility needs checking";
break;
case "outside scope":
headline = "Fast: Unavailable for this model/provider";
break;
}
const rows = [headline];
if (status.lastObserved !== undefined) {
const labels: Record<ObservedTier, string> = {
default: "Standard",
priority: "Priority",
flex: "Flex",
auto: "Automatic",
fast: "Fast",
};
rows.push(`Last reported tier: ${labels[status.lastObserved]}`);
} else if (status.requestPolicy === "priority") {
rows.push("No response tier reported yet.");
}
if (status.conflict) rows.push("Last request kept another tier setting; not overridden.");
if (status.requestPolicy === "priority") rows.push("Priority requests use more allowance.");
return rows.join("\n");
}
+101 -102
View File
@@ -34,19 +34,22 @@ const catalog: CodexModelCatalog = {
};
describe("commands and ownership", () => {
it.each(["", " STATUS "])(
"reports %j without confirmation, auth IO, catalog IO or writes",
async (args) => {
it.each(["", " ", "status", " STATUS ", "reset", "enable", "disable", "on extra"])(
"rejects the unsupported action %j without confirmation, IO or writes",
async (action) => {
const fast = createFastControllerFixture();
fast.ctx.sessionManager.getBranch.mockReturnValue([choice("on")]);
fast.controller.restore(fast.ctx);
await fast.controller.handle(args, fast.ctx);
await fast.controller.handle(action, fast.ctx);
expect(fast.ctx.ui.notify).toHaveBeenCalledExactlyOnceWith(
expect.stringContaining(
"Preference: on\nEligibility: unknown\nNext request policy: suspended",
),
"info",
"Usage: /eleith:codex fast [on|off|toggle]",
"warning",
);
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
preference: "on",
eligibility: "unknown",
requestPolicy: "suspended",
});
expect(fast.ctx.ui.confirm).not.toHaveBeenCalled();
expect(fast.readAuthScope).not.toHaveBeenCalled();
expect(fast.ctx.modelRegistry.getProviderAuth).not.toHaveBeenCalled();
@@ -93,12 +96,11 @@ describe("commands and ownership", () => {
const fast = createFastControllerFixture();
await fast.controller.handle("on extra", fast.ctx);
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
"Usage: /eleith:codex fast [on|off|toggle|status|reset]",
"Usage: /eleith:codex fast [on|off|toggle]",
"warning",
);
fast.ctx.isIdle.mockReturnValue(false);
for (const action of ["on", "off", "toggle", "reset"])
await fast.controller.handle(action, fast.ctx);
for (const action of ["on", "off", "toggle"]) await fast.controller.handle(action, fast.ctx);
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.stringContaining("Wait for the current run"),
"warning",
@@ -164,7 +166,7 @@ describe("commands and ownership", () => {
service_tier: "priority",
});
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.stringContaining("Next request policy: priority"),
expect.stringContaining("Fast: On (priority requested)"),
"info",
);
});
@@ -187,7 +189,7 @@ describe("commands and ownership", () => {
},
);
it("copies a frozen payload for explicit default, preserves fields, then resets with an inherit tombstone", async () => {
it("copies a frozen payload for explicit default, preserves fields, then restores a legacy inherit entry", async () => {
const fast = createFastControllerFixture();
const input = Object.freeze([{ role: "user", content: "hello" }]);
const payload = Object.freeze({
@@ -198,6 +200,10 @@ describe("commands and ownership", () => {
max_output_tokens: 42,
});
await fast.controller.handle("off", fast.ctx);
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
"Fast: Off (standard requested)\n\n/eleith:codex fast on|off|toggle",
"info",
);
const transformed = fast.controller.transform(payload, fast.ctx);
expect(transformed).toEqual({ ...payload, service_tier: "default" });
expect(transformed).not.toBe(payload);
@@ -209,40 +215,36 @@ describe("commands and ownership", () => {
).toBeUndefined();
await fast.controller.handle("off", fast.ctx);
expect(fast.appendEntry).toHaveBeenCalledOnce();
await fast.controller.handle("reset", fast.ctx);
expect(fast.appendEntry).toHaveBeenLastCalledWith(FAST_ENTRY, {
provider: model.provider,
modelId: model.id,
mode: "inherit",
});
fast.ctx.sessionManager.getBranch.mockReturnValue([choice("off"), choice("inherit")]);
fast.controller.restore(fast.ctx);
expect(fast.controller.transform(payload, fast.ctx)).toBeUndefined();
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.stringContaining(
"Preference: inherit\nEligibility: unknown\nNext request policy: untouched",
),
"info",
);
await fast.controller.handle("reset", fast.ctx);
expect(fast.appendEntry).toHaveBeenCalledTimes(2);
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
preference: "inherit",
requestPolicy: "untouched",
});
await fast.controller.prepare(fast.ctx);
fast.controller.restore(fast.ctx);
expect(fast.appendEntry).toHaveBeenCalledOnce();
expect(fast.ctx.ui.confirm).not.toHaveBeenCalled();
expect(fast.readAuthScope).not.toHaveBeenCalled();
expect(fast.fetchCatalog).not.toHaveBeenCalled();
});
it("can remove its saved choice after the selection leaves direct scope", async () => {
it("restores a legacy inherit entry after the selection leaves direct scope without writing", async () => {
const fast = createFastControllerFixture();
await fast.controller.handle("off", fast.ctx);
fast.ctx.modelRegistry.isUsingOAuth.mockReturnValue(false);
await fast.controller.handle("reset", fast.ctx);
expect(fast.appendEntry).toHaveBeenLastCalledWith(FAST_ENTRY, {
provider: model.provider,
modelId: model.id,
mode: "inherit",
fast.ctx.sessionManager.getBranch.mockReturnValue([choice("off"), choice("inherit")]);
fast.controller.restore(fast.ctx);
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
preference: "inherit",
requestPolicy: "outside scope",
});
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.stringContaining("Next request policy: outside scope"),
"info",
);
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
expect(fast.appendEntry).toHaveBeenCalledOnce();
expect(fast.ctx.ui.confirm).not.toHaveBeenCalled();
expect(fast.readAuthScope).not.toHaveBeenCalled();
expect(fast.fetchCatalog).not.toHaveBeenCalled();
});
it.each(["on", "off"])(
@@ -254,28 +256,18 @@ describe("commands and ownership", () => {
const payload = Object.freeze({ model: model.id, service_tier: "flex", input: [] });
expect(fast.controller.transform(payload, fast.ctx)).toBeUndefined();
expect(payload.service_tier).toBe("flex");
await fast.controller.handle("status", fast.ctx);
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.stringContaining("Tier conflict: an existing non-owned tier was left untouched."),
"info",
);
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.stringContaining("Last requested tier: unknown"),
"info",
);
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.stringContaining(`Next request policy: ${tier}`),
"info",
);
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
conflict: true,
requestPolicy: tier,
});
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toEqual({
model: model.id,
service_tier: tier,
});
await fast.controller.handle("status", fast.ctx);
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.not.stringContaining("Tier conflict:"),
"info",
);
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
conflict: false,
requestPolicy: tier,
});
},
);
@@ -344,13 +336,11 @@ describe("branch replay and preparation", () => {
stream({ type: "response.completed", response: { service_tier: "default" } }),
fast.ctx,
);
await fast.controller.handle("status", fast.ctx);
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.stringContaining(
"Preference: on\nEligibility: unknown\nNext request policy: suspended",
),
"info",
);
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
preference: "on",
eligibility: "unknown",
requestPolicy: "suspended",
});
expect(fast.readAuthScope).toHaveBeenCalledOnce();
expect(fast.fetchCatalog).toHaveBeenCalledOnce();
expect(fast.appendEntry).toHaveBeenCalledOnce();
@@ -397,13 +387,11 @@ describe("branch replay and preparation", () => {
await fast.controller.prepare(fast.ctx);
expect(fast.fetchCatalog).toHaveBeenCalledTimes(2);
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
await fast.controller.handle("status", fast.ctx);
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.stringContaining(
"Preference: on\nEligibility: unsupported\nNext request policy: suspended",
),
"info",
);
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
preference: "on",
eligibility: "unsupported",
requestPolicy: "suspended",
});
fast.readAuthScope.mockResolvedValue("scope-c");
fast.fetchCatalog.mockResolvedValue({ ...catalog, authScope: "scope-c" });
await fast.controller.prepare(fast.ctx);
@@ -453,7 +441,11 @@ describe("branch replay and preparation", () => {
stream({ type: "response.completed", response: { service_tier: "priority" } }),
fast.ctx,
);
await fast.controller.handle("status", fast.ctx);
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
preference: "on",
requestPolicy: "suspended",
lastObserved: "priority",
});
expect(fast.fetchCatalog).toHaveBeenCalledOnce();
expect(fast.readAuthScope).toHaveBeenCalledTimes(2);
await fast.controller.prepare(fast.ctx);
@@ -481,11 +473,10 @@ describe("branch replay and preparation", () => {
"warning",
);
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
await fast.controller.handle("status", fast.ctx);
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.stringContaining("Preference: on"),
"info",
);
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
preference: "on",
requestPolicy: "suspended",
});
expect(fast.appendEntry).not.toHaveBeenCalled();
},
);
@@ -594,7 +585,8 @@ describe("asynchronous ownership", () => {
expect(fast.readAuthScope).not.toHaveBeenCalled();
expect(fast.fetchCatalog).not.toHaveBeenCalled();
expect(fast.appendEntry).not.toHaveBeenCalled();
await fast.controller.handle("status", fast.ctx);
expect(fast.controller.getStatus(fast.ctx)).toBeUndefined();
await fast.controller.handle("on", fast.ctx);
await fast.controller.prepare(fast.ctx);
fast.controller.restore(fast.ctx);
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
@@ -663,22 +655,26 @@ describe("asynchronous ownership", () => {
});
describe("provider stream observations", () => {
it("reports an observed default independently of requested priority, without changing the next preference", async () => {
it("reports an observed default independently of the priority payload and next-request policy", async () => {
const fast = createFastControllerFixture();
await fast.controller.handle("on", fast.ctx);
const payload = Object.freeze({ model: model.id, input: [], service_tier: "priority" });
expect(fast.controller.transform(payload, fast.ctx)).toBeUndefined();
const transformed = fast.controller.transform(payload, fast.ctx);
expect(transformed).toBeUndefined();
expect(transformed ?? payload).toEqual({
model: model.id,
input: [],
service_tier: "priority",
});
fast.controller.observe(
stream({ type: "response.completed", response: { service_tier: "default" } }),
fast.ctx,
);
await fast.controller.handle("status", fast.ctx);
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.stringContaining(
"Next request policy: priority\nLast requested tier: priority\nLast observed tier: default",
),
"info",
);
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
preference: "on",
requestPolicy: "priority",
lastObserved: "default",
});
expect(fast.controller.transform({ model: model.id }, fast.ctx)?.service_tier).toBe("priority");
expect(fast.fetchCatalog).toHaveBeenCalledOnce();
expect(fast.readAuthScope).toHaveBeenCalledOnce();
@@ -690,17 +686,19 @@ describe("provider stream observations", () => {
])("reports missing or unrecognized terminal tier metadata as unknown: %j", async (data) => {
const fast = createFastControllerFixture();
await fast.controller.handle("off", fast.ctx);
fast.controller.transform({ model: model.id }, fast.ctx);
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toEqual({
model: model.id,
service_tier: "default",
});
fast.controller.observe(
stream({ type: "response.completed", response: { service_tier: "default" } }),
fast.ctx,
);
fast.controller.observe(stream(data), fast.ctx);
await fast.controller.handle("status", fast.ctx);
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.stringContaining("Last requested tier: default\nLast observed tier: unknown"),
"info",
);
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
requestPolicy: "default",
lastObserved: undefined,
});
expect(fast.readAuthScope).not.toHaveBeenCalled();
expect(fast.fetchCatalog).not.toHaveBeenCalled();
});
@@ -708,7 +706,10 @@ describe("provider stream observations", () => {
it("ignores wrong-model and nonterminal events, and does not show delivery from another session", async () => {
const fast = createFastControllerFixture();
await fast.controller.handle("off", fast.ctx);
fast.controller.transform({ model: model.id }, fast.ctx);
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toEqual({
model: model.id,
service_tier: "default",
});
fast.controller.observe(
stream({ type: "response.completed", response: { service_tier: "default" } }),
fast.ctx,
@@ -724,21 +725,19 @@ describe("provider stream observations", () => {
stream({ type: "response.created", response: { service_tier: "priority" } }),
fast.ctx,
);
await fast.controller.handle("status", fast.ctx);
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.stringContaining("Last observed tier: default"),
"info",
);
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
requestPolicy: "default",
lastObserved: "default",
});
fast.ctx.sessionManager.getSessionId.mockReturnValue("session-b");
fast.controller.observe(
stream({ type: "response.completed", response: { service_tier: "priority" } }),
fast.ctx,
);
await fast.controller.handle("status", fast.ctx);
expect(fast.ctx.ui.notify).toHaveBeenLastCalledWith(
expect.stringContaining("Last requested tier: unknown\nLast observed tier: unknown"),
"info",
);
expect(fast.controller.getStatus(fast.ctx)).toMatchObject({
requestPolicy: "default",
lastObserved: undefined,
});
});
});
+35 -42
View File
@@ -18,9 +18,9 @@ import {
} from "./fast-state.ts";
import {
formatFastStatus,
type FastStatus,
type FastRequestPolicy,
type ObservedTier,
type RequestedTier,
} from "./fast-status.ts";
import { modelKey } from "./profiles.ts";
import {
@@ -65,7 +65,6 @@ interface CachedEligibility {
interface ModelTierActivity {
modelKey: string;
sessionId: string;
requested: RequestedTier | undefined;
observed: ObservedTier | undefined;
conflict: boolean;
}
@@ -93,15 +92,36 @@ export class FastController {
this.preferences = readFastPreferences(ctx.sessionManager.getBranch());
}
getStatus(ctx: FastHost): FastStatus | undefined {
if (this.disposed) return undefined;
const model = ctx.model;
if (!model) return undefined;
const selectedModelKey = modelKey(model);
const direct = isDirectCodexSelection(ctx);
let activity = this.modelTierActivity;
if (
!activity ||
!direct ||
activity.modelKey !== selectedModelKey ||
activity.sessionId !== ctx.sessionManager.getSessionId()
)
activity = undefined;
let eligibility: FastEligibility = "unknown";
if (direct) eligibility = this.currentEligibility(selectedModelKey);
return {
preference: this.preferenceFor(selectedModelKey),
eligibility,
requestPolicy: this.requestPolicy(ctx),
lastObserved: activity?.observed,
conflict: activity?.conflict ?? false,
};
}
async handle(args: string, ctx: FastHost): Promise<void> {
if (this.disposed) return;
const action = args.trim().toLowerCase();
if (!action || action === "status") {
this.report(ctx);
return;
}
if (!["on", "off", "toggle", "reset"].includes(action)) {
ctx.ui.notify(`Usage: /${this.commandName} fast [on|off|toggle|status|reset]`, "warning");
if (!["on", "off", "toggle"].includes(action)) {
ctx.ui.notify(`Usage: /${this.commandName} fast [on|off|toggle]`, "warning");
return;
}
if (this.pendingPreparation || !ctx.isIdle()) {
@@ -113,18 +133,11 @@ export class FastController {
ctx.ui.notify("No model selected.", "warning");
return;
}
let mode: FastPreference;
if (action === "reset") mode = "inherit";
else if (action === "off") mode = "off";
let mode: "on" | "off";
if (action === "off") mode = "off";
else if (action === "toggle" && this.preferenceFor(modelKey(model)) === "on") mode = "off";
else mode = "on";
// Reset only removes our choice; it is safe even after the selection leaves supported scope.
if (mode === "inherit") {
this.rememberChoice(ctx, mode);
this.report(ctx);
return;
}
if (!isDirectCodexSelection(ctx)) {
ctx.ui.notify(
"Fast controls require a direct official legacy Codex model with Pi OAuth and no provider override.",
@@ -172,7 +185,6 @@ export class FastController {
const activity: ModelTierActivity = {
modelKey: modelKey(ctx.model),
sessionId: ctx.sessionManager.getSessionId(),
requested: undefined,
observed: undefined,
conflict: false,
};
@@ -183,7 +195,6 @@ export class FastController {
activity.conflict = true;
return;
}
activity.requested = policy;
if (payload.service_tier === policy) return;
return { ...payload, service_tier: policy };
}
@@ -385,32 +396,14 @@ export class FastController {
}
private report(ctx: FastHost): void {
const model = ctx.model;
if (!model) {
const status = this.getStatus(ctx);
if (!status) {
ctx.ui.notify("No model selected.", "info");
return;
}
const selectedModelKey = modelKey(model);
const direct = isDirectCodexSelection(ctx);
let activity = this.modelTierActivity;
if (
!activity ||
!direct ||
activity.modelKey !== selectedModelKey ||
activity.sessionId !== ctx.sessionManager.getSessionId()
)
activity = undefined;
let eligibility: FastEligibility = "unknown";
if (direct) eligibility = this.currentEligibility(selectedModelKey);
const status = {
preference: this.preferenceFor(selectedModelKey),
eligibility,
requestPolicy: this.requestPolicy(ctx),
lastRequested: activity?.requested,
lastObserved: activity?.observed,
conflict: activity?.conflict ?? false,
};
ctx.ui.notify(formatFastStatus(status), "info");
const summary = formatFastStatus(status);
const hint = `/${this.commandName} fast on|off|toggle`;
ctx.ui.notify(`${summary}\n\n${hint}`, "info");
}
}
+112 -28
View File
@@ -1,6 +1,7 @@
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
import type { Api, Model } from "@earendil-works/pi-ai";
import { afterAll, expect, it, vi } from "vitest";
import codex from "./index.ts";
import { fetchCodexModelCatalog, fetchCodexUsage, readCodexAuthScope } from "./client.ts";
@@ -88,26 +89,38 @@ it("registers only the configured native command once after binding", async () =
await context.emit("session_start");
expect(context.pi.registerCommand).toHaveBeenCalledTimes(1);
expect(context.pi.registerCommand.mock.calls[0]?.[0]).toBe("personal:codex");
expect(await context.command().getArgumentCompletions?.("e")).toEqual([
{ value: "extend", label: "extend" },
expect(await context.command().getArgumentCompletions?.("c")).toEqual([
expect.objectContaining({
value: "context",
label: "context",
description: expect.any(String),
}),
]);
expect(await context.command().getArgumentCompletions?.("")).toEqual([
{ value: "extend", label: "extend" },
{ value: "restore", label: "restore" },
{ value: "usage", label: "usage" },
{ value: "fast", label: "fast" },
const rootChoices = await context.command().getArgumentCompletions?.("");
expect(rootChoices?.map(({ value }) => value)).toEqual(["context", "fast", "usage"]);
expect(await context.command().getArgumentCompletions?.("fast o")).toEqual([
expect.objectContaining({
value: "fast on",
description: expect.stringContaining("allowance"),
}),
expect.objectContaining({
value: "fast off",
description: expect.stringContaining("standard"),
}),
]);
});
it("handles no argument as status rather than an invalid default action", async () => {
const context = await load();
await context.emit("session_start");
await context.command().handler("", context.ctx);
expect(context.notify).toHaveBeenLastCalledWith("No model selected.", "info");
for (const input of ["", "context", "fast"]) {
await context.command().handler(input, context.ctx);
expect(context.notify).toHaveBeenLastCalledWith("No model selected.", "info");
}
expect(context.pi.setModel).not.toHaveBeenCalled();
await context.command().handler("extend restore", context.ctx);
expect(context.notify).toHaveBeenLastCalledWith(
"Usage: /eleith:codex [extend|restore|usage|fast]",
"Usage: /eleith:codex [context|fast|usage]",
"warning",
);
});
@@ -130,21 +143,24 @@ it("wires restore/reconcile hooks without provider calls for an unsupported mode
expect(readCodexAuthScope).not.toHaveBeenCalled();
});
it("routes fast subcommands without allowing trailing arguments on other actions", async () => {
it("rejects removed commands and additional control arguments without side effects", async () => {
const context = await load("personal");
await context.emit("session_start");
await context.command().handler("fast status", context.ctx);
expect(context.notify).toHaveBeenLastCalledWith("No model selected.", "info");
await context.command().handler("fast off extra", context.ctx);
expect(context.notify).toHaveBeenLastCalledWith(
"Usage: /personal:codex fast [on|off|toggle|status|reset]",
"warning",
);
await context.command().handler("restore extra", context.ctx);
expect(context.notify).toHaveBeenLastCalledWith(
"Usage: /personal:codex [extend|restore|usage|fast]",
"warning",
);
const rootUsage = "Usage: /personal:codex [context|fast|usage]";
const cases = [
["extend", rootUsage],
["restore", rootUsage],
["usage extra", rootUsage],
["fast status", "Usage: /personal:codex fast [on|off|toggle]"],
["fast reset", "Usage: /personal:codex fast [on|off|toggle]"],
["fast off extra", "Usage: /personal:codex fast [on|off|toggle]"],
["context status", "Usage: /personal:codex context [on|off|toggle]"],
["context on extra", "Usage: /personal:codex context [on|off|toggle]"],
] as const;
for (const [input, usage] of cases) {
await context.command().handler(input, context.ctx);
expect(context.notify).toHaveBeenLastCalledWith(usage, "warning");
}
expect(fetchCodexModelCatalog).not.toHaveBeenCalled();
expect(readCodexAuthScope).not.toHaveBeenCalled();
expect(fetchCodexUsage).not.toHaveBeenCalled();
@@ -164,10 +180,9 @@ it("routes usage through the configured command without requiring an idle Codex
context.ctx.modelRegistry,
expect.any(AbortSignal),
);
expect(context.notify).toHaveBeenCalledExactlyOnceWith(
expect.stringContaining("Plan: plus"),
"info",
);
expect(context.notify).toHaveBeenNthCalledWith(1, "Fetching Codex usage…", "info");
expect(context.notify).toHaveBeenLastCalledWith(expect.stringContaining("Plan: plus"), "info");
expect(context.notify).toHaveBeenCalledTimes(2);
expect(context.pi.setModel).not.toHaveBeenCalled();
expect(context.pi.appendEntry).not.toHaveBeenCalled();
});
@@ -189,10 +204,79 @@ it.each(["model_select", "session_tree", "session_shutdown"])(
expect(signal.aborted).toBe(true);
finish({ plan_type: "plus" });
await request;
expect(context.notify).not.toHaveBeenCalled();
expect(context.notify).toHaveBeenCalledExactlyOnceWith("Fetching Codex usage…", "info");
},
);
it("combines context and fast mode in a read-only dashboard with the configured command hints", async () => {
const context = await load("personal");
await context.emit("session_start");
const model: Model<Api> = {
id: "gpt-6-sol",
name: "Sol",
provider: "openai-codex",
api: "openai-codex-responses",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 272_000,
maxTokens: 128_000,
};
const getProviderAuth = vi.fn(async () => {
throw new Error("Auth lookup forbidden");
});
const ctx = {
...context.ctx,
model,
modelRegistry: {
find: () => model,
getProvider: () => ({ baseUrl: model.baseUrl }),
isUsingOAuth: () => true,
getRegisteredProviderConfig: () => undefined,
getRegisteredNativeProvider: () => undefined,
getProviderAuth,
},
} as unknown as ExtensionCommandContext;
await context.command().handler("", ctx);
expect(context.notify).toHaveBeenCalledExactlyOnceWith(
expect.stringContaining("Fast: Provider default (no override)"),
"info",
);
expect(context.notify).toHaveBeenLastCalledWith(expect.stringContaining("Context:"), "info");
expect(context.notify).toHaveBeenLastCalledWith(
expect.stringContaining("/personal:codex fast"),
"info",
);
expect(getProviderAuth).not.toHaveBeenCalled();
expect(fetchCodexUsage).not.toHaveBeenCalled();
expect(fetchCodexModelCatalog).not.toHaveBeenCalled();
expect(readCodexAuthScope).not.toHaveBeenCalled();
expect(context.pi.appendEntry).not.toHaveBeenCalled();
expect(context.pi.setModel).not.toHaveBeenCalled();
const dashboard = context.notify.mock.calls[0][0];
for (const input of ["context", "fast"]) {
await context.command().handler(input, ctx);
expect(context.notify).toHaveBeenLastCalledWith(dashboard, "info");
}
expect(context.notify).toHaveBeenCalledTimes(3);
expect(getProviderAuth).not.toHaveBeenCalled();
expect(fetchCodexUsage).not.toHaveBeenCalled();
expect(fetchCodexModelCatalog).not.toHaveBeenCalled();
expect(readCodexAuthScope).not.toHaveBeenCalled();
expect(context.pi.appendEntry).not.toHaveBeenCalled();
expect(context.pi.setModel).not.toHaveBeenCalled();
await context.emit("session_shutdown");
const notify = context.notify;
Object.defineProperty(ctx, "ui", {
get: () => {
throw new Error("Expired UI");
},
});
for (const input of ["", "context", "fast"]) await context.command().handler(input, ctx);
expect(notify).toHaveBeenCalledTimes(3);
});
it("fails invalid prefix settings before registering any capabilities", async () => {
writeFileSync(configPath, '{"commandPrefix":"Invalid Prefix"}');
const on = vi.fn(),
+22 -6
View File
@@ -4,6 +4,8 @@ import { getSettings } from "@eleith/pi-internal/config";
import { ContextController } from "./controller.ts";
import { UsageController } from "./usage-controller.ts";
import { FastController } from "./fast.ts";
import { CODEX_ACTIONS, completeCodexArguments } from "./commands.ts";
import { formatCodexDashboard } from "./dashboard.ts";
export default async function codex(pi: ExtensionAPI): Promise<void> {
const settings = await getSettings();
@@ -11,17 +13,27 @@ export default async function codex(pi: ExtensionAPI): Promise<void> {
const controller = new ContextController(pi, commandName);
const usage = new UsageController();
const fast = new FastController(pi, commandName);
let disposed = false;
registerCommand(pi, {
name: commandName,
description: "Codex context, account usage and fast controls",
actions: ["extend", "restore", "usage", "fast"],
argumentActions: ["fast"],
description: "Context, fast requests and account allowance",
actions: CODEX_ACTIONS,
getArgumentCompletions: completeCodexArguments,
argumentActions: ["context", "fast"],
defaultAction: "",
handler(action, ctx, argument) {
if (disposed) return;
const showStatus = !action || ((action === "context" || action === "fast") && !argument);
if (showStatus) {
const context = controller.getStatus(ctx);
const fastStatus = fast.getStatus(ctx);
ctx.ui.notify(formatCodexDashboard(context, fastStatus, commandName), "info");
return;
}
if (action === "usage") return usage.report(ctx);
if (action === "context") return controller.handle(argument ?? "", ctx);
if (action === "fast") return fast.handle(argument ?? "", ctx);
return controller.handle(action, ctx);
},
});
@@ -40,9 +52,13 @@ export default async function codex(pi: ExtensionAPI): Promise<void> {
return controller.modelSelected(ctx);
});
pi.on("session_shutdown", (_event, ctx) => {
usage.dispose();
disposed = true;
fast.dispose();
controller.shutdown(ctx);
try {
usage.dispose();
} finally {
controller.shutdown(ctx);
}
});
pi.on("input", async (event, ctx) => {
if (event.streamingBehavior === undefined && ctx.isIdle()) {
+9 -9
View File
@@ -180,7 +180,7 @@ it.each([
const original = structuredClone(f.catalog);
try {
const leaf = f.manager.getLeafId();
await f.session.prompt("/eleith:codex extend");
await f.session.prompt("/eleith:codex context on");
const extended = f.session.model!;
expect(extended.contextWindow).toBe(Math.max(window, 922_000));
if (window < 922_000) {
@@ -194,7 +194,7 @@ it.each([
await f.session.reload();
expect(f.session.model?.contextWindow).toBe(Math.max(window, 922_000));
expect(f.registry.find("openai-codex", modelId)?.contextWindow).toBe(window);
await f.session.prompt("/eleith:codex restore");
await f.session.prompt("/eleith:codex context off");
expect(f.session.model?.contextWindow).toBe(window);
expect(extended.contextWindow).toBe(Math.max(window, 922_000));
expect(f.catalog).toEqual(original);
@@ -225,7 +225,7 @@ it("SDK replays a saved context preference after the command rename", async () =
expect(f.session.model?.contextWindow).toBe(922_000);
expect(f.preferences()).toEqual([saved]);
await f.session.prompt("/eleith:codex restore");
await f.session.prompt("/eleith:codex context off");
expect(f.session.model?.contextWindow).toBe(272_000);
expect(f.preferences()).toEqual([saved, { ...saved, extended: false }]);
} finally {
@@ -257,7 +257,7 @@ it.each(["rename", "external"])(
});
const selection = vi.spyOn(f.session, "setModel");
try {
const request = f.session.prompt("/eleith:codex extend");
const request = f.session.prompt("/eleith:codex context on");
await started;
const submitted = selection.mock.calls[0][0];
const originalSubmitted = structuredClone(submitted);
@@ -284,7 +284,7 @@ it.each(["rename", "external"])(
expect(thinkingWrites).not.toHaveBeenCalled();
expect(f.session.thinkingLevel).toBe(thinking);
// Only an explicit context-changing command resumes work.
await f.session.prompt("/eleith:codex restore");
await f.session.prompt("/eleith:codex context off");
expect(f.session.model?.contextWindow).toBe(272_000);
expect(f.preferences()).toEqual([
{ provider: "openai-codex", modelId: "gpt-6-sol", extended: false },
@@ -304,14 +304,14 @@ it.each(["missing", "endpoint"])(
async (reason) => {
const f = await fixture();
try {
await f.session.prompt("/eleith:codex extend");
await f.session.prompt("/eleith:codex context on");
const lookup = vi
.spyOn(f.registry, "find")
.mockReturnValue(
reason === "missing" ? undefined : { ...f.catalog, baseUrl: "https://different.invalid" },
);
const select = vi.spyOn(f.session, "setModel");
await f.session.prompt("/eleith:codex restore");
await f.session.prompt("/eleith:codex context off");
expect(select).not.toHaveBeenCalled();
expect(f.session.model?.contextWindow).toBe(922_000);
expect(f.confirm).not.toHaveBeenCalled();
@@ -359,9 +359,9 @@ it.each(["user", "custom"] as const)(
return stream;
});
try {
await f.session.prompt("/eleith:codex extend");
await f.session.prompt("/eleith:codex context on");
const extendedLeaf = f.manager.getLeafId()!;
await f.session.prompt("/eleith:codex restore");
await f.session.prompt("/eleith:codex context off");
expect(f.session.model?.contextWindow).toBe(272_000);
expect(f.confirm).toHaveBeenCalledTimes(1);
+331 -144
View File
@@ -1,7 +1,7 @@
import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
import type { ContextUsage, SessionEntry } from "@earendil-works/pi-coding-agent";
import { describe, expect, it } from "vitest";
import { formatContextStatus } from "./status.ts";
import { type ContextStatus, formatContextSummary } from "./status.ts";
const NOW = Date.parse("2026-01-10T12:00:00Z");
const model: Model<Api> = {
@@ -16,7 +16,7 @@ const model: Model<Api> = {
contextWindow: 272_000,
maxTokens: 128_000,
};
const status: Parameters<typeof formatContextStatus>[0] = {
const status: ContextStatus = {
model,
usage: undefined,
standardWindow: 272_000,
@@ -74,81 +74,258 @@ function compaction(id: string, timestamp: string, parentId: string | null = nul
};
}
describe("context status windows", () => {
it("reports the selected model and local extended metadata without changing it", () => {
const current = {
describe("context summary occupancy", () => {
it("uses the physical 128k window for a routed model advertising an extended 1m window", () => {
const report = formatContextSummary(
{
...status,
model: { ...model, contextWindow: 1_000_000 },
extendedWindow: 1_000_000,
desiredExtended: true,
usage: { tokens: 120_000, contextWindow: 128_000, percent: 93.75 },
},
NOW,
);
expect(report.split("\n")[0]).toBe(
"Context: 120k / 128k tokens (effective) · Selected context window: 1m tokens (extended)",
);
expect(report).not.toContain("pending");
});
it.each([undefined, 0])(
"keeps known effective occupancy when the selected window is %s",
(contextWindow) => {
// A routed or persisted model may lack today's required window metadata.
const selected = { ...model, contextWindow } as unknown as Model<Api>;
const report = formatContextSummary(
{
...status,
model: selected,
usage: { tokens: 120_000, contextWindow: 128_000, percent: 93.75 },
},
NOW,
);
expect(report.split("\n")[0]).toBe(
"Context: 120k / 128k tokens (effective) · Selected context window: unknown tokens (window unknown)",
);
},
);
it("provides three read-only local lines without a model header or fast hint", () => {
const current: ContextStatus = {
...status,
extendedWindow: 922_000,
usage: { tokens: 12_000, contextWindow: 922_000, percent: 1.3 },
usage: { tokens: 40_000, contextWindow: 272_000, percent: 14.7 },
branch: [
compaction("first", "2026-01-10T11:50:00Z"),
compaction("last", "2026-01-10T11:55:00Z", "first"),
message(assistant({ input: 10_000, cacheRead: 20_000 })),
],
};
const original = structuredClone(current);
const report = formatContextStatus(current, NOW);
expect(report).toContain("codex · openai-codex/gpt-6-sol");
expect(report).toMatch(/^ Usage +12k \/ 922k$/m);
expect(report).toMatch(/^ Standard +272k$/m);
expect(report).toMatch(/^ Extended +922k \(local window\)$/m);
expect(formatContextSummary(current, NOW)).toBe(
"Context: 40k / 272k tokens (standard)\n" +
"Compaction: 2 on this branch · last 5 minutes ago\n" +
"Cached input: 20k / 30k (last assistant request)",
);
expect(current).toEqual(original);
});
it("shows unknown usage with the model window when usage is unavailable", () => {
const report = formatContextStatus(status, NOW);
expect(report).toMatch(/^ Usage +unknown \/ 272k$/m);
expect(report).toMatch(/^ Extended +not verified$/m);
});
it.each([null, -1, NaN, Infinity])("does not turn invalid token usage %s into zero", (tokens) => {
const report = formatContextStatus(
{ ...status, usage: { tokens, contextWindow: 922_000, percent: null } },
it.each([
[272_000, "standard"],
[922_000, "extended"],
[500_000, "custom"],
] as const)("keeps the selected %d window's %s label separate", (contextWindow, label) => {
const report = formatContextSummary(
{
...status,
model: { ...model, contextWindow },
extendedWindow: 922_000,
usage: { tokens: 12_000, contextWindow: 128_000, percent: 9.375 },
},
NOW,
);
expect(report).toMatch(/^ Usage +unknown \/ 922k$/m);
});
it("preserves genuinely zero context usage", () => {
const report = formatContextStatus(
{ ...status, usage: { tokens: 0, contextWindow: 272_000, percent: 0 } },
NOW,
expect(report.split("\n")[0]).toBe(
`Context: 12k / 128k tokens (effective) · Selected context window: ${contextWindow / 1000}k tokens (${label})`,
);
expect(report).toMatch(/^ Usage +0 \/ 272k$/m);
});
});
describe("catalog baseline and desired state", () => {
it("reports unknown baseline honestly without inventing an extended target", () => {
const report = formatContextStatus({ ...status, standardWindow: undefined }, NOW);
expect(report).toMatch(/^ Standard +unknown$/m);
expect(report).toContain("unknown; reselect a catalog model");
});
it("explains an already extended-sized catalog baseline", () => {
const report = formatContextStatus(
{ ...status, standardWindow: 1_000_000, extendedWindow: 1_000_000 },
NOW,
);
expect(report).toContain("catalog already provides this window; no extension needed");
});
it.each([true, false])(
"keeps desired preference %s separate from effective usage and replay pause",
(desiredExtended) => {
const report = formatContextStatus({ ...status, desiredExtended, replayPaused: true }, NOW);
expect(report).toContain(
`${desiredExtended ? "extended" : "standard"} (desired, not a receipt)`,
it.each([undefined, null, 0, -1, NaN, Infinity])(
"falls back to the selected window when the effective window is %s",
(contextWindow) => {
const usage = { tokens: 12_000, contextWindow, percent: null } as unknown as ContextUsage;
expect(formatContextSummary({ ...status, usage }, NOW).split("\n")[0]).toBe(
"Context: 12k / 272k tokens (standard)",
);
expect(report).toMatch(/^ Usage +unknown \/ 272k$/m);
expect(report).toContain("paused; explicitly extend or restore");
},
);
});
describe("context status compactions", () => {
it("reports no compaction on an empty branch", () => {
const report = formatContextStatus(status, NOW);
expect(report).toMatch(/^ Compactions +0 on this branch$/m);
expect(report).toMatch(/^ Last compacted +never on this branch$/m);
it.each([null, -1, NaN, Infinity])("keeps invalid token usage %s unknown", (tokens) => {
const report = formatContextSummary(
{ ...status, usage: { tokens, contextWindow: 272_000, percent: null } },
NOW,
);
expect(report.split("\n")[0]).toBe("Context: 272k tokens (standard) · usage not reported");
});
it("counts only supplied branch compactions and uses the last in branch order", () => {
it("retains the effective window even when token usage is unknown", () => {
const report = formatContextSummary(
{ ...status, usage: { tokens: null, contextWindow: 128_000, percent: null } },
NOW,
);
expect(report.split("\n")[0]).toBe(
"Context: 128k tokens (effective) · usage not reported · Selected context window: 272k tokens (standard)",
);
});
it("does not fabricate a zero window when neither window is known", () => {
const report = formatContextSummary(
{
...status,
model: { ...model, contextWindow: 0 },
usage: { tokens: 12_000, contextWindow: 0, percent: null },
},
NOW,
);
expect(report.split("\n")[0]).toBe("Context: 12k / unknown tokens (window unknown)");
});
it("preserves measured zero usage and keeps an empty branch compact", () => {
expect(
formatContextSummary(
{ ...status, usage: { tokens: 0, contextWindow: 272_000, percent: 0 } },
NOW,
),
).toBe(
"Context: 0 / 272k tokens (standard)\n" +
"Compaction: 0 on this branch\n" +
"Cached input: not reported",
);
});
});
describe("context summary baselines and desired state", () => {
it("does not infer a baseline from the model name or a verified extended window", () => {
const report = formatContextSummary(
{
...status,
model: { ...model, contextWindow: 922_000 },
standardWindow: undefined,
extendedWindow: 922_000,
},
NOW,
);
expect(report.split("\n")[0]).toBe(
"Context: 922k tokens (baseline unknown) · usage not reported",
);
});
it("labels a catalog already at the extended target standard with no pending replay", () => {
const report = formatContextSummary(
{
...status,
model: { ...model, contextWindow: 1_000_000 },
standardWindow: 1_000_000,
extendedWindow: 1_000_000,
desiredExtended: true,
},
NOW,
);
expect(report.split("\n")[0]).toBe("Context: 1m tokens (standard) · usage not reported");
});
it("does not call a larger custom window extended without verified metadata", () => {
const report = formatContextSummary(
{ ...status, model: { ...model, contextWindow: 922_000 } },
NOW,
);
expect(report.split("\n")[0]).toBe("Context: 922k tokens (custom) · usage not reported");
});
it.each([
[true, 272_000, "standard", "extended"],
[false, 922_000, "extended", "standard"],
] as const)(
"does not present desired extended=%s as active",
(desiredExtended, contextWindow, active, desired) => {
const current = {
...status,
model: { ...model, contextWindow },
extendedWindow: 922_000,
desiredExtended,
};
expect(formatContextSummary(current, NOW).split("\n")[0]).toBe(
`Context: ${contextWindow / 1000}k tokens (${active}) · usage not reported · ${desired} pending normal prompt`,
);
const paused = formatContextSummary({ ...current, replayPaused: true }, NOW);
expect(paused.split("\n")[0]).toBe(
`Context: ${contextWindow / 1000}k tokens (${active}) · usage not reported · ${desired} pending · replay paused`,
);
expect(paused).not.toContain("normal prompt");
},
);
it("keeps pending replay tied to the selected window, not a matching physical window", () => {
const current: ContextStatus = {
...status,
model: { ...model, contextWindow: 922_000 },
desiredExtended: false,
usage: { tokens: 120_000, contextWindow: 128_000, percent: 93.75 },
standardWindow: 128_000,
extendedWindow: 922_000,
};
expect(formatContextSummary(current, NOW).split("\n")[0]).toBe(
"Context: 120k / 128k tokens (effective) · Selected context window: 922k tokens (extended) · standard pending normal prompt",
);
expect(formatContextSummary({ ...current, replayPaused: true }, NOW).split("\n")[0]).toBe(
"Context: 120k / 128k tokens (effective) · Selected context window: 922k tokens (extended) · standard pending · replay paused",
);
});
it.each([
[false, 272_000, "standard"],
[true, 922_000, "extended"],
] as const)(
"omits pending when desired extended=%s matches the selected window",
(desiredExtended, contextWindow, label) => {
const report = formatContextSummary(
{
...status,
model: { ...model, contextWindow },
extendedWindow: 922_000,
desiredExtended,
},
NOW,
);
expect(report.split("\n")[0]).toBe(
`Context: ${contextWindow / 1000}k tokens (${label}) · usage not reported`,
);
},
);
it.each([true, false])(
"does not promise replay with an unknown target (%s)",
(desiredExtended) => {
const report = formatContextSummary(
{ ...status, standardWindow: undefined, desiredExtended, replayPaused: true },
NOW,
);
expect(report.split("\n")[0]).toBe(
"Context: 272k tokens (baseline unknown) · usage not reported · " +
`${desiredExtended ? "extended" : "standard"} desired (target unknown) · replay paused`,
);
expect(report).not.toContain("normal prompt");
},
);
it("retains the replay pause even without a saved preference", () => {
expect(formatContextSummary({ ...status, replayPaused: true }, NOW).split("\n")[0]).toBe(
"Context: 272k tokens (standard) · usage not reported · replay paused",
);
});
});
describe("context summary branch compactions", () => {
it("counts only the supplied active branch and uses branch order rather than timestamps", () => {
const shared = compaction("shared", "2026-01-10T11:59:59Z");
const abandoned = compaction("abandoned", "2026-01-10T11:00:00Z", "shared");
const active = compaction("active", "2026-01-08T12:00:00Z", "shared");
@@ -160,12 +337,12 @@ describe("context status compactions", () => {
fromId: "abandoned",
summary: "Branch summary",
};
expect(formatContextStatus({ ...status, branch: [shared, abandoned] }, NOW)).toMatch(
/^ Last compacted +1 hour ago$/m,
);
const report = formatContextStatus({ ...status, branch: [shared, active, summary] }, NOW);
expect(report).toMatch(/^ Compactions +2 on this branch$/m);
expect(report).toMatch(/^ Last compacted +2 days ago$/m);
expect(
formatContextSummary({ ...status, branch: [shared, abandoned] }, NOW).split("\n")[1],
).toBe("Compaction: 2 on this branch · last 1 hour ago");
expect(
formatContextSummary({ ...status, branch: [shared, active, summary] }, NOW).split("\n")[1],
).toBe("Compaction: 2 on this branch · last 2 days ago");
});
it.each([
@@ -179,35 +356,33 @@ describe("context status compactions", () => {
[86_399_999, "23 hours ago"],
[86_400_000, "1 day ago"],
[172_800_000, "2 days ago"],
])("formats elapsed time at %d ms", (age, expected) => {
const branch = [compaction("compact", new Date(NOW - age).toISOString())];
expect(formatContextStatus({ ...status, branch }, NOW)).toMatch(
new RegExp(`^ Last compacted +${expected}$`, "m"),
])("reports the final branch compaction's age at %d ms", (age, expected) => {
const branch = [compaction("last", new Date(NOW - age).toISOString())];
expect(formatContextSummary({ ...status, branch }, NOW).split("\n")[1]).toBe(
`Compaction: 1 on this branch · last ${expected}`,
);
});
it.each(["not-a-date", new Date(NOW + 1000).toISOString()])(
"reports unknown elapsed time for %s",
"does not invent the last compaction time for %s",
(timestamp) => {
const report = formatContextStatus(
{ ...status, branch: [compaction("compact", timestamp)] },
NOW,
const branch = [compaction("last", timestamp)];
expect(formatContextSummary({ ...status, branch }, NOW).split("\n")[1]).toBe(
"Compaction: 1 on this branch · last time unknown",
);
expect(report).toMatch(/^ Compactions +1 on this branch$/m);
expect(report).toMatch(/^ Last compacted +unknown$/m);
},
);
});
describe("context status cache reports", () => {
describe("context summary last-request cache", () => {
it("does not invent cache metrics without an assistant request", () => {
const branch = [message({ role: "user", content: "Hello", timestamp: NOW })];
const report = formatContextStatus({ ...status, branch }, NOW);
expect(report).toMatch(/^ Cached input +not reported$/m);
expect(report).not.toMatch(/^ Cache writes /m);
expect(formatContextSummary({ ...status, branch }, NOW).split("\n")[2]).toBe(
"Cached input: not reported",
);
});
it("adds disjoint uncached, cache-read and cache-write counts, excluding output", () => {
it("includes disjoint cache writes in the input total but excludes output", () => {
const branch = [
message(
assistant({
@@ -219,40 +394,40 @@ describe("context status cache reports", () => {
}),
),
];
const report = formatContextStatus({ ...status, branch }, NOW);
expect(report).toMatch(/^ Cached input +6k \/ 9k input tokens$/m);
expect(report).toMatch(/^ Cache writes +1k tokens$/m);
expect(report).not.toMatch(/^ Model /m);
const report = formatContextSummary({ ...status, branch }, NOW);
expect(report.split("\n")).toHaveLength(3);
expect(report.split("\n")[2]).toBe("Cached input: 6k / 9k · wrote 1k (last assistant request)");
});
it("allows cache reads to account for all input", () => {
const branch = [message(assistant({ cacheRead: 1000 }))];
expect(formatContextStatus({ ...status, branch }, NOW)).toMatch(
/^ Cached input +1k \/ 1k input tokens$/m,
expect(formatContextSummary({ ...status, branch }, NOW).split("\n")[2]).toBe(
"Cached input: 1k / 1k (last assistant request)",
);
});
it.each([0, 1000])("calls zero cache reads none reported with %d cache writes", (cacheWrite) => {
const branch = [message(assistant({ input: 2000, cacheWrite }))];
const report = formatContextStatus({ ...status, branch }, NOW);
expect(report).toMatch(
new RegExp(
`^ Cached input +none reported \\(${cacheWrite ? "3k" : "2k"} input tokens\\)$`,
"m",
),
);
if (cacheWrite) expect(report).toMatch(/^ Cache writes +1k tokens$/m);
else expect(report).not.toMatch(/^ Cache writes /m);
});
it.each([
{ cacheWrite: 0, expected: "none reported · 2k input tokens" },
{ cacheWrite: 1000, expected: "none reported · 3k input tokens · wrote 1k" },
])(
"does not call zero cache reads a measured miss with $cacheWrite writes",
({ cacheWrite, expected }) => {
const branch = [message(assistant({ input: 2000, cacheWrite }))];
expect(formatContextSummary({ ...status, branch }, NOW).split("\n")[2]).toBe(
`Cached input: ${expected} (last assistant request)`,
);
},
);
it("treats an all-zero failed request as unreported rather than a cache miss", () => {
const branch = [
message(assistant({}, { stopReason: "error", errorMessage: "Request failed" })),
];
const report = formatContextStatus({ ...status, branch }, NOW);
expect(report).toMatch(/^ Cached input +not reported$/m);
expect(report).not.toContain("none reported");
});
it.each(["stop", "error"] as const)(
"keeps an all-zero %s request unreported rather than claiming a cache miss",
(stopReason) => {
const branch = [message(assistant({}, { stopReason }))];
expect(formatContextSummary({ ...status, branch }, NOW).split("\n")[2]).toBe(
"Cached input: not reported (last assistant request)",
);
},
);
it.each([
{ input: -1 },
@@ -266,49 +441,61 @@ describe("context status cache reports", () => {
const branch = [
message(assistant({ input: 2000, cacheRead: 6000, cacheWrite: 1000, ...usage })),
];
const report = formatContextStatus({ ...status, branch }, NOW);
expect(report).toMatch(/^ Cached input +not reported$/m);
expect(report).not.toMatch(/^ Cache writes /m);
expect(formatContextSummary({ ...status, branch }, NOW).split("\n")[2]).toBe(
"Cached input: not reported (last assistant request)",
);
});
it.each([undefined, { input: 2000, cacheWrite: 0 }])(
"tolerates missing persisted usage or counters: %j",
(usage) => {
// Persisted legacy/malformed messages can lack fields required by today's type.
// Legacy/malformed messages can lack fields required by today's type.
const persisted = { ...assistant(), usage } as unknown as AssistantMessage;
const report = formatContextStatus({ ...status, branch: [message(persisted)] }, NOW);
expect(report).toMatch(/^ Cached input +not reported$/m);
expect(
formatContextSummary({ ...status, branch: [message(persisted)] }, NOW).split("\n")[2],
).toBe("Cached input: not reported (last assistant request)");
},
);
it("uses the last assistant even after a model switch and trailing non-assistant entries", () => {
const branch: SessionEntry[] = [
message(assistant({ input: 1000, cacheRead: 9000 }), "earlier"),
message(
assistant({ input: 2000, cacheRead: 1000 }, { provider: "other", model: "prior-model" }),
"last",
),
{
type: "model_change",
id: "switch",
parentId: "last",
timestamp: new Date(NOW).toISOString(),
provider: model.provider,
modelId: model.id,
},
message({ role: "user", content: "Next request", timestamp: NOW }, "user"),
compaction("compact", new Date(NOW).toISOString()),
];
const report = formatContextStatus({ ...status, branch }, NOW);
expect(report).toMatch(/^ Model +other\/prior-model$/m);
expect(report).toMatch(/^ Cached input +1k \/ 3k input tokens$/m);
expect(report).not.toContain("9k / 10k");
});
it("still identifies the prior request model when its usage was not reported", () => {
const branch = [message(assistant({}, { provider: "other", model: "prior-model" }))];
const report = formatContextStatus({ ...status, branch }, NOW);
expect(report).toMatch(/^ Model +other\/prior-model$/m);
expect(report).toMatch(/^ Cached input +not reported$/m);
});
it.each([
{
scenario: "reported usage",
usage: { input: 2000, cacheRead: 1000 },
stopReason: "stop",
expected: "1k / 3k",
},
{
scenario: "a failed placeholder",
usage: {},
stopReason: "error",
expected: "not reported",
},
] as const)(
"scopes the latest request's $scenario to its prior model after a switch",
({ usage, stopReason, expected }) => {
const branch: SessionEntry[] = [
message(assistant({ input: 1000, cacheRead: 9000 }), "earlier"),
message(
assistant(usage, { provider: "other", model: "prior-model", stopReason }),
"latest",
),
{
type: "model_change",
id: "switch",
parentId: "latest",
timestamp: new Date(NOW).toISOString(),
provider: model.provider,
modelId: model.id,
},
message({ role: "user", content: "Next request", timestamp: NOW }, "user"),
compaction("last", new Date(NOW).toISOString()),
];
const report = formatContextSummary({ ...status, branch }, NOW);
expect(report.split("\n")[2]).toBe(
`Cached input: ${expected} (last assistant request · other/prior-model)`,
);
expect(report).not.toContain("9k / 10k");
expect(report).not.toContain("none reported");
},
);
});
+63 -72
View File
@@ -2,7 +2,7 @@ import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
import type { ContextUsage, SessionEntry } from "@earendil-works/pi-coding-agent";
import { modelKey } from "./profiles.ts";
interface ContextStatus {
export interface ContextStatus {
readonly model: Model<Api>;
readonly usage: ContextUsage | undefined;
readonly standardWindow: number | undefined;
@@ -21,12 +21,12 @@ function validCount(value: number | null | undefined): value is number {
return typeof value === "number" && Number.isFinite(value) && value >= 0;
}
function tokens(value: number | null | undefined): string {
return validCount(value) ? TOKEN_FORMAT.format(value).toLowerCase() : "unknown";
function validWindow(value: number | null | undefined): value is number {
return validCount(value) && value > 0;
}
function row(label: string, value: string): string {
return ` ${label.padEnd(18)}${value}`;
function tokens(value: number | null | undefined): string {
return validCount(value) ? TOKEN_FORMAT.format(value).toLowerCase() : "unknown";
}
function elapsed(timestamp: string, now: number): string {
@@ -50,87 +50,78 @@ function lastAssistant(branch: readonly SessionEntry[]): AssistantMessage | unde
return undefined;
}
function cacheRows(message: AssistantMessage | undefined, currentModel: Model<Api>): string[] {
if (!message) return [row("Cached input", "not reported")];
function cacheSummary(message: AssistantMessage | undefined, currentModel: Model<Api>): string {
if (!message) return "Cached input: not reported";
const rows: string[] = [];
const requestModel = modelKey({ provider: message.provider, id: message.model });
if (requestModel !== modelKey(currentModel)) rows.push(row("Model", requestModel));
const scope =
requestModel === modelKey(currentModel)
? "last assistant request"
: `last assistant request · ${requestModel}`;
const usage = message.usage;
// Pi normalizes input into disjoint uncached, cache-read, and cache-write counts.
// An all-zero placeholder (e.g. a failed request) is not evidence of a cache miss.
let value = "not reported";
if (
!usage ||
!validCount(usage.input) ||
!validCount(usage.cacheRead) ||
!validCount(usage.cacheWrite)
usage &&
validCount(usage.input) &&
validCount(usage.cacheRead) &&
validCount(usage.cacheWrite)
) {
return [...rows, row("Cached input", "not reported")];
// Pi's uncached, cache-read and cache-write counts are disjoint; output is excluded.
const input = usage.input + usage.cacheRead + usage.cacheWrite;
if (Number.isFinite(input) && input > 0) {
// Zero cache counters may be adapter defaults, not a measured cache miss.
value =
usage.cacheRead > 0
? `${tokens(usage.cacheRead)} / ${tokens(input)}`
: `none reported · ${tokens(input)} input tokens`;
if (usage.cacheWrite > 0) value += ` · wrote ${tokens(usage.cacheWrite)}`;
}
}
const input = usage.input + usage.cacheRead + usage.cacheWrite;
if (!Number.isFinite(input) || input === 0) return [...rows, row("Cached input", "not reported")];
// Some adapters also normalize absent cache counters to zero. Don't claim we
// know whether that means a measured miss or a provider without cache metrics.
rows.push(
row(
"Cached input",
usage.cacheRead > 0
? `${tokens(usage.cacheRead)} / ${tokens(input)} input tokens`
: `none reported (${tokens(input)} input tokens)`,
),
);
if (usage.cacheWrite > 0) rows.push(row("Cache writes", `${tokens(usage.cacheWrite)} tokens`));
return rows;
return `Cached input: ${value} (${scope})`;
}
/** Read-only report: no settings-file guesses, provider calls, or preference changes. */
export function formatContextStatus(status: ContextStatus, now = Date.now()): string {
const { model, usage, standardWindow, extendedWindow, branch, desiredExtended, replayPaused } =
function windowLabel({ model, standardWindow, extendedWindow }: ContextStatus): string {
if (!validWindow(model.contextWindow)) return "window unknown";
if (!validWindow(standardWindow)) return "baseline unknown";
if (model.contextWindow === standardWindow) return "standard";
if (validWindow(extendedWindow) && model.contextWindow === extendedWindow) return "extended";
return "custom";
}
/** Read-only local snapshot; the caller supplies the model header and fast-mode hint. */
export function formatContextSummary(status: ContextStatus, now = Date.now()): string {
const { model, usage, standardWindow, extendedWindow, desiredExtended, replayPaused, branch } =
status;
const lines = [
`codex · ${modelKey(model)}`,
"",
" window",
row(
"Usage",
`${tokens(usage?.tokens)} / ${tokens(usage?.contextWindow ?? model.contextWindow)}`,
),
row("Standard", tokens(standardWindow)),
];
const selectedWindow = validWindow(model.contextWindow) ? model.contextWindow : undefined;
// Routed/virtual models may advertise a different window from the one used for compaction.
const effectiveWindow = validWindow(usage?.contextWindow) ? usage.contextWindow : selectedWindow;
const separateWindows = effectiveWindow !== selectedWindow;
const label = separateWindows ? "effective" : windowLabel(status);
const window = `${tokens(effectiveWindow)} tokens (${label})`;
let context = validCount(usage?.tokens)
? `Context: ${tokens(usage.tokens)} / ${window}`
: `Context: ${window} · usage not reported`;
if (standardWindow === undefined) {
lines.push(row("Baseline", "unknown; reselect a catalog model"));
} else if (extendedWindow === standardWindow) {
lines.push(row("Baseline", "catalog already provides this window; no extension needed"));
if (separateWindows) {
context += ` · Selected context window: ${tokens(selectedWindow)} tokens (${windowLabel(status)})`;
}
if (desiredExtended !== undefined) {
const preference = desiredExtended ? "extended" : "standard";
lines.push(row("Branch preference", `${preference} (desired, not a receipt)`));
const desired = desiredExtended ? "extended" : "standard";
const target = desiredExtended ? extendedWindow : standardWindow;
if (!validWindow(target)) {
context += ` · ${desired} desired (target unknown)`;
} else if (selectedWindow !== target) {
context += ` · ${desired} pending${replayPaused ? "" : " normal prompt"}`;
}
}
if (replayPaused) {
lines.push(row("Automatic replay", "paused; explicitly extend or restore"));
}
const extendedLabel =
extendedWindow === undefined ? "not verified" : `${tokens(extendedWindow)} (local window)`;
lines.push(row("Extended", extendedLabel));
if (replayPaused) context += " · replay paused";
const compactions = branch.filter((entry) => entry.type === "compaction");
const lastCompaction = compactions.at(-1);
const lastCompacted = lastCompaction
? elapsed(lastCompaction.timestamp, now)
: "never on this branch";
lines.push(
"",
" compaction",
row("Compactions", `${compactions.length} on this branch`),
row("Last compacted", lastCompacted),
"",
" cache · last assistant request",
...cacheRows(lastAssistant(branch), model),
);
return lines.join("\n");
let compaction = `Compaction: ${compactions.length} on this branch`;
if (lastCompaction) {
const age = elapsed(lastCompaction.timestamp, now);
compaction += age === "unknown" ? " · last time unknown" : ` · last ${age}`;
}
return [context, compaction, cacheSummary(lastAssistant(branch), model)].join("\n");
}
+408 -123
View File
@@ -1,6 +1,384 @@
import { expect, it, vi } from "vitest";
import { rmSync } from "node:fs";
import { afterAll, afterEach, expect, it, vi } from "vitest";
import { BorderedLoader, initTheme } from "@earendil-works/pi-coding-agent";
import { CodexAccountError, type CodexAccountRegistry, type fetchCodexUsage } from "./client.ts";
import { UsageController, type UsageHost } from "./usage-controller.ts";
import { customInteractionUI } from "./custom-interaction.test-helper.ts";
const directory = await vi.hoisted(async () => {
const { mkdtempSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const directory = mkdtempSync(join(tmpdir(), "codex-usage-controller-test-"));
vi.stubEnv("PI_CODING_AGENT_DIR", directory);
return directory;
});
initTheme("dark", false);
const cleanup: (() => void)[] = [];
afterEach(() => {
for (const close of cleanup.splice(0)) close();
vi.useRealTimers();
});
afterAll(() => {
vi.unstubAllEnvs();
rmSync(directory, { recursive: true, force: true });
});
it("renders and animates the native bordered loader, then closes before notifying", async () => {
vi.useFakeTimers();
const f = fixture();
const response = deferred<unknown>();
f.fetchUsage.mockReturnValueOnce(response.promise);
const request = f.controller.report(f.ctx);
await Promise.resolve();
const modal = f.ctx.modals[0];
const loader = modal.component!;
expect(loader).toBeInstanceOf(BorderedLoader);
const lines = loader.render(60);
expect(lines[0]).toBe("─".repeat(60));
expect(lines.at(-1)).toBe("─".repeat(60));
expect(lines.join("\n")).toContain("Fetching Codex usage…");
expect(lines.join("\n")).toMatch(/esc.*cancel/i);
const renderCount = f.ctx.requestRender.mock.calls.length;
vi.advanceTimersByTime(160);
expect(f.ctx.requestRender.mock.calls.length).toBeGreaterThan(renderCount);
expect(loader.render(60)).not.toEqual(lines);
expect(f.ctx.ui.notify).not.toHaveBeenCalled();
expect(f.fetchUsage).toHaveBeenCalledExactlyOnceWith(
f.ctx.modelRegistry,
expect.any(AbortSignal),
);
const signal = f.fetchUsage.mock.calls[0][1]!;
const removeAbort = vi.spyOn(signal, "removeEventListener");
const removeLoaderAbort = vi.spyOn((loader as BorderedLoader).signal, "removeEventListener");
const notify = f.ctx.ui.notify;
notify.mockImplementation(() => {
expect(modal.closed).toBe(true);
expect(modal.close).toHaveBeenCalledOnce();
expect(signal.aborted).toBe(false);
expire(f.ctx);
});
response.resolve({ plan_type: "plus" });
await request;
expect(notify).toHaveBeenCalledExactlyOnceWith(expect.stringContaining("Plan: plus"), "info");
expect(removeAbort).toHaveBeenCalledWith("abort", expect.any(Function));
expect(removeLoaderAbort).toHaveBeenCalledWith("abort", expect.any(Function));
const stoppedCount = f.ctx.requestRender.mock.calls.length;
vi.advanceTimersByTime(160);
expect(f.ctx.requestRender).toHaveBeenCalledTimes(stoppedCount);
});
it.each(["success", "error"] as const)(
"Escape aborts and closes immediately even when fetch ignores abort, ignoring late %s",
async (outcome) => {
const f = fixture();
const response = deferred<unknown>();
f.fetchUsage.mockReturnValueOnce(response.promise);
const request = f.controller.report(f.ctx);
await Promise.resolve();
const modal = f.ctx.modals[0];
const signal = f.fetchUsage.mock.calls[0][1]!;
const loader = modal.component as BorderedLoader;
const removeAbort = vi.spyOn(signal, "removeEventListener");
const removeLoaderAbort = vi.spyOn(loader.signal, "removeEventListener");
loader.handleInput("\u001b");
expect(loader.signal.aborted).toBe(true);
expect(signal.aborted).toBe(true);
expect(modal.closed).toBe(true);
expect(modal.close).toHaveBeenCalledOnce();
await request; // No need to settle the uncooperative fetch.
expect(removeAbort).toHaveBeenCalledWith("abort", expect.any(Function));
expect(removeLoaderAbort).toHaveBeenCalledWith("abort", expect.any(Function));
const notify = f.ctx.ui.notify;
expire(f.ctx);
if (outcome === "error") response.reject(new Error("private late failure"));
else response.resolve({ plan_type: "late-plan" });
await response.promise.catch(() => {});
expect(notify).not.toHaveBeenCalled();
expect(modal.close).toHaveBeenCalledOnce();
},
);
it("treats external component disposal as cancellation, not just an animation stop", async () => {
const f = fixture();
const response = deferred<unknown>();
f.fetchUsage.mockReturnValueOnce(response.promise);
const request = f.controller.report(f.ctx);
await Promise.resolve();
const modal = f.ctx.modals[0];
const loader = modal.component as BorderedLoader;
loader.dispose();
expect(loader.signal.aborted).toBe(false); // Native disposal itself does not abort.
expect(f.fetchUsage.mock.calls[0][1]!.aborted).toBe(true);
expect(modal.closed).toBe(true);
await request;
expect(f.ctx.ui.notify).not.toHaveBeenCalled();
response.resolve({ plan_type: "late" });
await response.promise;
expect(modal.close).toHaveBeenCalledOnce();
});
it.each(["cancel", "dispose"] as const)(
"%s closes only the owned modal with no expired context access or late delivery",
async (action) => {
for (const fails of [false, true]) {
const f = fixture();
const response = deferred<unknown>();
f.fetchUsage.mockReturnValueOnce(response.promise);
const request = f.controller.report(f.ctx);
await Promise.resolve();
const modal = f.ctx.modals[0];
const signal = f.fetchUsage.mock.calls[0][1]!;
const notify = f.ctx.ui.notify;
expire(f.ctx);
f.controller[action]();
expect(signal.aborted).toBe(true);
expect(modal.closed).toBe(true);
expect(modal.close).toHaveBeenCalledOnce();
f.controller[action]();
await request;
if (fails) response.reject(new Error("private late failure"));
else response.resolve({ plan_type: "late" });
await response.promise.catch(() => {});
expect(notify).not.toHaveBeenCalled();
expect(modal.close).toHaveBeenCalledOnce();
if (action === "dispose") {
await f.controller.report(f.ctx);
expect(f.fetchUsage).toHaveBeenCalledOnce();
}
}
},
);
it("supersedes before adoption and stops the old animation before any manual disposal", async () => {
vi.useFakeTimers();
const f = fixture();
const first = deferred<unknown>();
const second = deferred<unknown>();
f.fetchUsage.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise);
const oldRequest = f.controller.report(f.ctx);
const oldModal = f.ctx.modals[0];
const oldSignal = f.fetchUsage.mock.calls[0][1]!;
const oldNotify = f.ctx.ui.notify;
expire(f.ctx);
const fresh = host("tui", f.ctx.surface);
const newRequest = f.controller.report(fresh);
expect(oldSignal.aborted).toBe(true);
expect(oldModal.closed).toBe(true);
await oldRequest;
await Promise.resolve();
const newModal = fresh.modals[1];
expect(oldModal.adopted).toBe(false);
expect(newModal.adopted).toBe(true);
expect(newModal.closed).toBe(false);
expect(f.ctx.surface.focusedComponent).toBe(newModal.component);
expect(f.ctx.surface.components).toEqual([f.ctx.surface.mainEditor, newModal.component]);
const oldRenderCount = oldModal.requestRender.mock.calls.length;
const newRenderCount = newModal.requestRender.mock.calls.length;
vi.advanceTimersByTime(320);
// This must precede manual disposal/fixture cleanup: early done is not adopted by Pi.
expect(oldModal.requestRender).toHaveBeenCalledTimes(oldRenderCount);
expect(newModal.requestRender.mock.calls.length).toBeGreaterThan(newRenderCount);
oldModal.component!.dispose?.();
(oldModal.component as BorderedLoader).handleInput("\u001b");
first.reject(new CodexAccountError("http", "Late rate limit", 60_000));
await first.promise.catch(() => {});
expect(newModal.closed).toBe(false);
expect(newModal.close).not.toHaveBeenCalled();
expect(oldNotify).not.toHaveBeenCalled();
second.resolve({ plan_type: "new-plan" });
await newRequest;
expect(newModal.close).toHaveBeenCalledOnce();
expect(fresh.ui.notify).toHaveBeenCalledExactlyOnceWith(
expect.stringContaining("Plan: new-plan"),
"info",
);
await f.controller.report(fresh);
expect(f.fetchUsage).toHaveBeenCalledTimes(3); // The stale rate limit did not apply.
});
it.each([
["complete", false],
["complete", true],
["cancel", false],
["cancel", true],
] as const)(
"old usage %s preserves a newer interaction (overlay: %s) on the same UI surface",
async (action, overlay) => {
const f = fixture();
const response = deferred<unknown>();
f.fetchUsage.mockReturnValueOnce(response.promise);
const request = f.controller.report(f.ctx);
await Promise.resolve();
const usageModal = f.ctx.modals[0];
const surface = f.ctx.surface;
expect(f.ctx.ui.custom).toHaveBeenCalledWith(expect.any(Function), { overlay: true });
expect(surface.focusedComponent).toBe(usageModal.component);
expect(surface.components).toEqual([surface.mainEditor, usageModal.component]);
const newerComponent = {
render: () => ["newer interaction"],
invalidate() {},
dispose: vi.fn(),
};
const newerRequest = f.ctx.ui.custom<void>(() => newerComponent, { overlay });
await Promise.resolve();
const newerModal = f.ctx.modals[1];
expect(surface.focusedComponent).toBe(newerComponent);
expect(surface.editorComponent).toBe(overlay ? surface.mainEditor : newerComponent);
expect(surface.components).toContain(usageModal.component);
expect(surface.components).toContain(newerComponent);
if (action === "cancel") f.controller.cancel();
else response.resolve({ plan_type: "plus" });
await request;
expect(usageModal.closed).toBe(true);
expect(newerModal.closed).toBe(false);
expect(newerModal.close).not.toHaveBeenCalled();
expect(newerComponent.dispose).not.toHaveBeenCalled();
expect(surface.focusedComponent).toBe(newerComponent);
expect(surface.editorComponent).toBe(overlay ? surface.mainEditor : newerComponent);
expect(surface.components).toEqual(
overlay ? [surface.mainEditor, newerComponent] : [newerComponent],
);
expect(newerModal.lines).toEqual(["newer interaction"]);
// The newer interaction still owns its completion and can return focus normally.
newerModal.close();
await newerRequest;
expect(newerComponent.dispose).toHaveBeenCalledOnce();
expect(surface.focusedComponent).toBe(surface.mainEditor);
expect(surface.components).toEqual([surface.mainEditor]);
if (action === "cancel") {
response.resolve({ plan_type: "late" });
await response.promise;
expect(f.ctx.ui.notify).not.toHaveBeenCalled();
}
},
);
it("suppresses a report when the session changes without an explicit cancellation", async () => {
const f = fixture();
const response = deferred<unknown>();
f.fetchUsage.mockReturnValueOnce(response.promise);
const request = f.controller.report(f.ctx);
f.ctx.sessionManager.getSessionId.mockReturnValue("replacement-session");
response.resolve({ plan_type: "plus" });
await request;
expect(f.ctx.modals[0].closed).toBe(true);
expect(f.ctx.ui.notify).not.toHaveBeenCalled();
f.controller.cancel();
expect(f.ctx.modals[0].close).toHaveBeenCalledOnce();
});
it.each([
[
"unrecognized response",
{ private_account_data: "do-not-display" },
undefined,
"Codex account service returned an unrecognized usage response.",
],
[
"unexpected error",
undefined,
new Error("private failure details"),
"Could not read Codex account usage.",
],
[
"account error",
undefined,
new CodexAccountError("timeout", "Codex account request timed out."),
"Codex account request timed out.",
],
] as const)("closes before reporting an %s", async (_name, data, error, message) => {
const f = fixture();
const response = deferred<unknown>();
f.fetchUsage.mockReturnValueOnce(response.promise);
const request = f.controller.report(f.ctx);
f.ctx.ui.notify.mockImplementation(() => {
expect(f.ctx.modals[0].closed).toBe(true);
expect(f.ctx.modals[0].close).toHaveBeenCalledOnce();
});
if (error) response.reject(error);
else response.resolve(data);
await request;
expect(f.ctx.ui.notify).toHaveBeenCalledExactlyOnceWith(message, "warning");
});
it("honors retry-after without polling or opening a loader for a blocked request", async () => {
const f = fixture();
f.fetchUsage.mockRejectedValueOnce(
new CodexAccountError("http", "Codex account service is rate limited.", 60_000),
);
await f.controller.report(f.ctx);
await f.controller.report(f.ctx);
expect(f.fetchUsage).toHaveBeenCalledOnce();
expect(f.ctx.ui.custom).toHaveBeenCalledOnce();
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(
"Codex account service is rate limited. Try again in 60s.",
"warning",
);
f.clock.now += 60_000;
await f.controller.report(f.ctx);
expect(f.fetchUsage).toHaveBeenCalledTimes(2);
expect(f.ctx.ui.custom).toHaveBeenCalledTimes(2);
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("Plan: plus"), "info");
});
it.each(["rpc", "print", "json"] as const)(
"%s uses only progress notifications, never custom terminal UI",
async (mode) => {
for (const fails of [false, true]) {
const f = fixture(mode);
if (fails) f.fetchUsage.mockRejectedValueOnce(new Error("private failure"));
const request = f.controller.report(f.ctx);
expect(f.ctx.ui.notify).toHaveBeenCalledExactlyOnceWith("Fetching Codex usage…", "info");
await request;
expect(f.ctx.ui.custom).not.toHaveBeenCalled();
expect(f.ctx.ui.notify).toHaveBeenCalledTimes(2);
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(
fails ? "Could not read Codex account usage." : expect.stringContaining("Plan: plus"),
fails ? "warning" : "info",
);
}
},
);
it.each(["rpc", "print"] as const)(
"%s cancellation settles without waiting, and another request can start",
async (mode) => {
const f = fixture(mode);
const response = deferred<unknown>();
f.fetchUsage.mockReturnValueOnce(response.promise);
const oldRequest = f.controller.report(f.ctx);
f.controller.cancel();
await oldRequest;
expect(f.fetchUsage.mock.calls[0][1]!.aborted).toBe(true);
await f.controller.report(f.ctx);
expect(f.ctx.ui.notify.mock.calls).toEqual([
["Fetching Codex usage…", "info"],
["Fetching Codex usage…", "info"],
[expect.stringContaining("Plan: plus"), "info"],
]);
response.reject(new Error("late failure"));
await response.promise.catch(() => {});
expect(f.ctx.ui.notify).toHaveBeenCalledTimes(3);
expect(f.ctx.ui.custom).not.toHaveBeenCalled();
},
);
it("propagates a final notification failure without touching the invalidated context again", async () => {
const f = fixture();
f.ctx.ui.notify.mockImplementation(() => {
expire(f.ctx);
throw new Error("UI failure");
});
await expect(f.controller.report(f.ctx)).rejects.toThrow("UI failure");
expect(f.ctx.modals[0].closed).toBe(true);
f.controller.cancel();
expect(f.ctx.modals[0].close).toHaveBeenCalledOnce();
});
function deferred<T>() {
let resolve!: (value: T) => void;
@@ -12,135 +390,42 @@ function deferred<T>() {
return { promise, resolve, reject };
}
function fixture() {
const clock = { now: Date.parse("2026-01-10T12:00:00Z") };
const fetchUsage = vi.fn<typeof fetchCodexUsage>(async () => ({ plan_type: "plus" }));
const ctx = {
function host(mode: UsageHost["mode"] = "tui", surface = customInteractionUI()) {
cleanup.push(() => surface.dispose());
return {
mode,
modelRegistry: {} as CodexAccountRegistry,
sessionManager: { getSessionId: vi.fn(() => "session") },
ui: { notify: vi.fn() },
} satisfies UsageHost;
ui: {
notify: vi.fn<UsageHost["ui"]["notify"]>(),
custom: surface.custom,
},
modals: surface.modals,
requestRender: surface.requestRender,
surface,
} satisfies UsageHost & {
modals: typeof surface.modals;
requestRender: typeof surface.requestRender;
surface: typeof surface;
};
}
function fixture(mode: UsageHost["mode"] = "tui") {
const clock = { now: Date.parse("2026-01-10T12:00:00Z") };
const fetchUsage = vi.fn<typeof fetchCodexUsage>(async () => ({ plan_type: "plus" }));
const ctx = host(mode);
const controller = new UsageController(fetchUsage, () => clock.now);
cleanup.push(() => controller.dispose());
return { controller, ctx, fetchUsage, clock };
}
it("reports the configured Codex account independently of the selected model", async () => {
const f = fixture();
await f.controller.report(f.ctx);
expect(f.fetchUsage).toHaveBeenCalledExactlyOnceWith(
f.ctx.modelRegistry,
expect.any(AbortSignal),
);
expect(f.ctx.ui.notify).toHaveBeenCalledExactlyOnceWith(
expect.stringContaining("codex account usage · openai-codex"),
"info",
);
});
it("supersedes an earlier request and ignores its late reply", async () => {
const f = fixture();
const first = deferred<unknown>();
f.fetchUsage.mockReturnValueOnce(first.promise).mockResolvedValueOnce({ plan_type: "new-plan" });
const oldRequest = f.controller.report(f.ctx);
const oldSignal = f.fetchUsage.mock.calls[0][1]!;
await f.controller.report(f.ctx);
expect(oldSignal.aborted).toBe(true);
first.resolve({ plan_type: "old-plan" });
await oldRequest;
expect(f.ctx.ui.notify).toHaveBeenCalledExactlyOnceWith(
expect.stringContaining("Plan: new-plan"),
"info",
);
});
it.each(["cancel", "dispose"] as const)(
"%s discards a late reply before accessing expired context",
async (action) => {
const f = fixture();
const response = deferred<unknown>();
f.fetchUsage.mockReturnValue(response.promise);
const request = f.controller.report(f.ctx);
const signal = f.fetchUsage.mock.calls[0][1]!;
const notify = f.ctx.ui.notify;
f.controller[action]();
Object.defineProperty(f.ctx, "sessionManager", {
function expire(ctx: UsageHost) {
for (const key of ["mode", "sessionManager", "modelRegistry", "ui"]) {
Object.defineProperty(ctx, key, {
get: () => {
throw new Error("Expired context");
},
configurable: true,
});
Object.defineProperty(f.ctx, "ui", {
get: () => {
throw new Error("Expired context");
},
});
response.resolve({ plan_type: "plus" });
await request;
expect(signal.aborted).toBe(true);
expect(notify).not.toHaveBeenCalled();
if (action === "dispose") await f.controller.report(f.ctx);
},
);
it("disposal suppresses a late error and is idempotent", async () => {
const f = fixture();
const response = deferred<unknown>();
f.fetchUsage.mockReturnValue(response.promise);
const request = f.controller.report(f.ctx);
f.controller.dispose();
f.controller.dispose();
response.reject(new Error("private late failure"));
await request;
await f.controller.report(f.ctx);
expect(f.ctx.ui.notify).not.toHaveBeenCalled();
expect(f.fetchUsage).toHaveBeenCalledOnce();
});
it("does not deliver a reply to a replacement session", async () => {
const f = fixture();
const response = deferred<unknown>();
f.fetchUsage.mockReturnValue(response.promise);
const request = f.controller.report(f.ctx);
f.ctx.sessionManager.getSessionId.mockReturnValue("different-session");
response.resolve({ plan_type: "plus" });
await request;
expect(f.ctx.ui.notify).not.toHaveBeenCalled();
});
it("reports an unrecognized schema without dumping account data", async () => {
const f = fixture();
f.fetchUsage.mockResolvedValue({ private_account_data: "do-not-display" });
await f.controller.report(f.ctx);
expect(f.ctx.ui.notify).toHaveBeenCalledExactlyOnceWith(
"Codex account service returned an unrecognized usage response.",
"warning",
);
});
it("does not expose unexpected error details", async () => {
const f = fixture();
f.fetchUsage.mockRejectedValue(new Error("private failure details"));
await f.controller.report(f.ctx);
expect(f.ctx.ui.notify).toHaveBeenCalledExactlyOnceWith(
"Could not read Codex account usage.",
"warning",
);
});
it("honors retry-after without polling or making another request until explicitly asked", async () => {
const f = fixture();
f.fetchUsage.mockRejectedValueOnce(
new CodexAccountError("http", "Codex account service is rate limited.", 60_000),
);
await f.controller.report(f.ctx);
await f.controller.report(f.ctx);
expect(f.fetchUsage).toHaveBeenCalledOnce();
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(
"Codex account service is rate limited. Try again in 60s.",
"warning",
);
f.clock.now += 60_000;
expect(f.fetchUsage).toHaveBeenCalledOnce();
await f.controller.report(f.ctx);
expect(f.fetchUsage).toHaveBeenCalledTimes(2);
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("Plan: plus"), "info");
});
}
}
+132 -21
View File
@@ -1,15 +1,25 @@
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
import { BorderedLoader, type ExtensionContext } from "@earendil-works/pi-coding-agent";
import { CodexAccountError, fetchCodexUsage, type CodexAccountRegistry } from "./client.ts";
import { formatCodexUsage } from "./usage.ts";
export interface UsageHost {
readonly mode: ExtensionContext["mode"];
readonly modelRegistry: CodexAccountRegistry;
readonly sessionManager: Pick<ExtensionContext["sessionManager"], "getSessionId">;
readonly ui: Pick<ExtensionContext["ui"], "notify">;
readonly ui: Pick<ExtensionContext["ui"], "notify" | "custom">;
}
type UsageResult = { data: unknown } | { error: unknown };
interface PendingUsage {
readonly controller: AbortController;
readonly sessionId: string;
// Owns only this interaction's loader and completion callback, never its context.
closeLoader?: () => void;
}
export class UsageController {
private pending: AbortController | undefined;
private pending: PendingUsage | undefined;
private retryAt = 0;
private disposed = false;
@@ -26,34 +36,55 @@ export class UsageController {
ctx.ui.notify(`Codex account service is rate limited. Try again in ${seconds}s.`, "warning");
return;
}
const sessionId = ctx.sessionManager.getSessionId();
const operation = new AbortController();
const operation: PendingUsage = {
controller: new AbortController(),
sessionId: ctx.sessionManager.getSessionId(),
};
this.pending = operation;
let result: UsageResult | undefined;
try {
const data = await this.fetchUsage(ctx.modelRegistry, operation.signal);
if (!this.canDeliverReport(operation, ctx, sessionId)) return;
const report = formatCodexUsage(data, this.now());
if (report === undefined) {
ctx.ui.notify("Codex account service returned an unrecognized usage response.", "warning");
const registry = ctx.modelRegistry;
if (ctx.mode === "tui") {
result = await this.readUsageWithLoader(operation, registry, ctx.ui);
} else {
ctx.ui.notify(report, "info");
// RPC has UI, but cannot host terminal components. Print/JSON use this too.
ctx.ui.notify("Fetching Codex usage…", "info");
result = await this.readUsageUntilCancelled(operation, registry);
}
} catch (error) {
if (!this.canDeliverReport(operation, ctx, sessionId)) return;
const message =
this.cancelOperation(operation);
throw error;
}
// The custom interaction has closed before any final notification or context access.
if (!this.canDeliverReport(operation, ctx) || result === undefined) {
this.cancelOperation(operation);
return;
}
let message: string;
let level: "info" | "warning" = "warning";
if ("error" in result) {
const error = result.error;
message =
error instanceof CodexAccountError ? error.message : "Could not read Codex account usage.";
if (error instanceof CodexAccountError && error.retryAfterMs !== undefined) {
this.retryAt = this.now() + error.retryAfterMs;
}
ctx.ui.notify(message, "warning");
} finally {
if (this.pending === operation) this.pending = undefined;
} else {
const report = formatCodexUsage(result.data, this.now());
message = report ?? "Codex account service returned an unrecognized usage response.";
if (report !== undefined) level = "info";
}
// A notification may reload the extension. Nothing may use ctx after this call.
this.pending = undefined;
ctx.ui.notify(message, level);
}
cancel(): void {
this.pending?.abort();
this.pending = undefined;
const operation = this.pending;
if (operation) this.cancelOperation(operation);
}
dispose(): void {
@@ -62,8 +93,88 @@ export class UsageController {
this.cancel();
}
private canDeliverReport(operation: AbortController, ctx: UsageHost, sessionId: string): boolean {
if (this.disposed || this.pending !== operation || operation.signal.aborted) return false;
return ctx.sessionManager.getSessionId() === sessionId;
private readUsageWithLoader(
operation: PendingUsage,
registry: CodexAccountRegistry,
ui: UsageHost["ui"],
): Promise<UsageResult | undefined> {
// An owned overlay closes only itself; Pi 1.3 non-overlay completion resets the
// editor/focus even when a newer interaction has taken over the shared surface.
return ui.custom<UsageResult | undefined>(
(tui, theme, _kb, done) => {
const loader = new BorderedLoader(tui, theme, "Fetching Codex usage…");
let closed = false;
const onLoaderAbort = () => this.cancelOperation(operation);
const closeLoader = (result?: UsageResult) => {
if (closed) return;
closed = true;
operation.closeLoader = undefined;
loader.signal.removeEventListener("abort", onLoaderAbort);
// Dispose explicitly too: Pi may receive done before adopting the factory result.
loader.dispose();
done(result);
};
operation.closeLoader = () => closeLoader();
loader.signal.addEventListener("abort", onLoaderAbort, { once: true });
// Native dispose stops animation, but does not abort its signal. External disposal
// must cancel our HTTP wait; disposal during successful close must not cancel delivery.
const disposeLoader = loader.dispose.bind(loader);
loader.dispose = () => {
disposeLoader();
if (!closed) this.cancelOperation(operation);
};
if (this.pending !== operation || operation.controller.signal.aborted) closeLoader();
else void this.readUsageUntilCancelled(operation, registry).then(closeLoader);
return loader;
},
{ overlay: true },
);
}
private readUsageUntilCancelled(
operation: PendingUsage,
registry: CodexAccountRegistry,
): Promise<UsageResult | undefined> {
const { signal } = operation.controller;
// Do not depend on an injected fetch implementation observing AbortSignal. Its late
// fulfillment/rejection is observed, but can neither retain ctx nor reopen/close UI.
return new Promise((resolve) => {
let settled = false;
const finishRead = (result?: UsageResult) => {
if (settled) return;
settled = true;
signal.removeEventListener("abort", onReadAbort);
resolve(result);
};
const onReadAbort = () => finishRead();
signal.addEventListener("abort", onReadAbort, { once: true });
if (signal.aborted) {
finishRead();
return;
}
try {
void this.fetchUsage(registry, signal).then(
(data) => finishRead({ data }),
(error: unknown) => finishRead({ error }),
);
} catch (error) {
finishRead({ error });
}
});
}
private cancelOperation(operation: PendingUsage): void {
if (this.pending === operation) this.pending = undefined;
operation.controller.abort();
operation.closeLoader?.();
}
private canDeliverReport(operation: PendingUsage, ctx: UsageHost): boolean {
// Invalidation must win before touching any potentially expired context getter.
if (this.disposed || this.pending !== operation || operation.controller.signal.aborted)
return false;
return ctx.sessionManager.getSessionId() === operation.sessionId;
}
}
+41 -6
View File
@@ -8,14 +8,17 @@ import {
type OAuthCredential,
} from "@earendil-works/pi-ai";
import {
BorderedLoader,
createAgentSession,
DefaultResourceLoader,
initTheme,
ModelRuntime,
SessionManager,
SettingsManager,
type ExtensionUIContext,
} from "@earendil-works/pi-coding-agent";
import codex from "./index.ts";
import { customInteractionUI } from "./custom-interaction.test-helper.ts";
const directory = await vi.hoisted(async () => {
const { mkdtempSync } = await import("node:fs");
@@ -52,19 +55,40 @@ it("SDK usage reuses Pi's OAuth without a model turn or credential write", async
vi.stubGlobal("fetch", accountRequest);
const { runtime, model, accessToken, credentialWrites, modelTurns } =
await createOfflineCodexRuntime();
const { session, sessionManager, notify, extensionErrors, closeSession } =
await openCodexExtensionSession(runtime, model);
const {
session,
sessionManager,
notify,
setStatus,
custom,
modals,
extensionErrors,
closeSession,
} = await openCodexExtensionSession(runtime, model);
try {
const branch = structuredClone(sessionManager.getBranch());
await session.prompt("/eleith:codex");
expect(accountRequest).not.toHaveBeenCalled();
notify.mockImplementation(() => {
expect(modals[0].closed).toBe(true);
expect(modals[0].close).toHaveBeenCalledOnce();
});
await session.prompt("/eleith:codex usage");
expect(custom).toHaveBeenCalledOnce();
expect(custom).toHaveBeenCalledWith(expect.any(Function), { overlay: true });
expect(modals[0].component).toBeInstanceOf(BorderedLoader);
expect(modals[0].lines.join("\n")).toContain("Fetching Codex usage…");
expect(modals[0].lines.join("\n")).toMatch(/esc.*cancel/i);
expect(notify).toHaveBeenLastCalledWith(
expect.stringContaining("Primary (5h): 25% used"),
expect.stringContaining("5h window: 75% remaining (25% used)"),
"info",
);
expect(accountRequest).toHaveBeenCalledOnce();
expect(setStatus).not.toHaveBeenCalled();
expect(modals[0].close.mock.invocationCallOrder[0]).toBeLessThan(
notify.mock.invocationCallOrder.at(-1)!,
);
const headers = new Headers(accountRequest.mock.calls[0][1]?.headers);
expect(headers.get("authorization")).toBe(`Bearer ${accessToken}`);
expect(headers.get("chatgpt-account-id")).toBe("sdk-test-account");
@@ -118,6 +142,7 @@ async function createOfflineCodexRuntime() {
}
async function openCodexExtensionSession(runtime: ModelRuntime, model: Model<Api>) {
initTheme("dark", false);
const settingsManager = SettingsManager.inMemory({
packages: [],
enableInstallTelemetry: false,
@@ -150,20 +175,30 @@ async function openCodexExtensionSession(runtime: ModelRuntime, model: Model<Api
noTools: "all",
});
const notify = vi.fn();
const setStatus = vi.fn();
const surface = customInteractionUI();
const { custom, modals } = surface;
const extensionErrors: unknown[] = [];
await session.bindExtensions({
mode: "tui",
uiContext: { notify } as unknown as ExtensionUIContext,
uiContext: { notify, setStatus, custom } as unknown as ExtensionUIContext,
onError: (error) => extensionErrors.push(error),
});
return {
session,
sessionManager,
notify,
setStatus,
custom,
modals,
extensionErrors,
async closeSession() {
await session.extensionRunner.emit({ type: "session_shutdown", reason: "quit" });
session.dispose();
try {
await session.extensionRunner.emit({ type: "session_shutdown", reason: "quit" });
} finally {
surface.dispose();
session.dispose();
}
},
};
}
+209 -30
View File
@@ -1,4 +1,4 @@
import { expect, it } from "vitest";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { formatCodexUsage } from "./usage.ts";
const NOW = Date.parse("2026-01-10T12:00:00Z");
@@ -7,8 +7,19 @@ const primary = {
limit_window_seconds: 1800,
reset_at: NOW / 1000 + 3600,
};
const LocalDateTimeFormat = Intl.DateTimeFormat;
let timeZone = "UTC";
it("formats an exact account report without mutating the payload", () => {
beforeEach(() => {
timeZone = "UTC";
// Pin only the test's local zone; production must use the user's local zone.
vi.spyOn(Intl, "DateTimeFormat").mockImplementation(function (locales, options) {
return new LocalDateTimeFormat(locales, { ...options, timeZone });
});
});
afterEach(() => vi.restoreAllMocks());
it("formats a remaining-first account report without mutating the payload", () => {
const payload = {
plan_type: "plus",
account_id: "do-not-display-account-id",
@@ -20,17 +31,16 @@ it("formats an exact account report without mutating the payload", () => {
const report = formatCodexUsage(payload, NOW)!;
expect(report).toBe(`codex account usage · openai-codex
Plan: plus
Credits: 12.5 balance (available)
Codex
Primary (30m): 25% used · 75% remaining
Resets: 2026-01-10 13:00:00 UTC (in 1h)
Secondary: not reported
Credits: 12.5 balance (available)`);
30m window: 75% remaining (25% used)
Resets: Jan 10, 2026, 13:00:00 UTC (in 1h)
Secondary window: not reported`);
expect(payload).toEqual(original);
});
it("does not invent windows or unlimited credits for plans with missing quota data", () => {
it("summarizes absent data without inventing windows or unlimited credits", () => {
const report = formatCodexUsage(
{
plan_type: "pro",
@@ -40,18 +50,31 @@ it("does not invent windows or unlimited credits for plans with missing quota da
},
NOW,
)!;
expect(report).toContain("Codex\n Not reported");
expect(report).toContain("Code review\n Not reported");
expect(report).toContain("Credits: not reported");
expect(report).toBe(`codex account usage · openai-codex
Plan: pro
Not reported: Codex quota, Code review quota, credits.`);
expect(report).not.toMatch(/0%|5h|7d|unlimited/);
});
it("does not add an unadvertised review quota or default windows to a plan-only report", () => {
expect(formatCodexUsage({ plan_type: "plus" }, NOW)).toBe(
"codex account usage · openai-codex\nPlan: plus\n\nNot reported: Codex quota, credits.",
);
});
it.each([
[0, "0% used · 100% remaining"],
[-1, "usage unknown"],
[101, "usage unknown"],
[NaN, "usage unknown"],
["25", "usage unknown"],
[0, "100% remaining (0% used)"],
[25, "75% remaining (25% used)"],
[100, "0% remaining (100% used)"],
[12.5, "87.5% remaining (12.5% used)"],
[-1, "remaining not reported"],
[101, "remaining not reported"],
[NaN, "remaining not reported"],
[Infinity, "remaining not reported"],
["25", "remaining not reported"],
[null, "remaining not reported"],
[undefined, "remaining not reported"],
] as const)("keeps invalid percentages distinct from genuine zero: %s", (used, expected) => {
const report = formatCodexUsage(
{
@@ -59,7 +82,43 @@ it.each([
},
NOW,
)!;
expect(report).toContain(expected);
expect(report).toContain(`30m window: ${expected}`);
if (expected === "remaining not reported") expect(report).not.toContain("%");
});
it.each([
[1800, "30m window"],
[18_000, "5h window"],
[604_800, "7d window"],
[90_061, "1d 1h 1m 1s window"],
[0, "Primary window (duration not reported)"],
[-1, "Primary window (duration not reported)"],
[1.5, "Primary window (duration not reported)"],
[Number.MAX_SAFE_INTEGER + 1, "Primary window (duration not reported)"],
["18000", "Primary window (duration not reported)"],
[undefined, "Primary window (duration not reported)"],
] as const)("labels only the actual advertised duration: %s", (seconds, expected) => {
const report = formatCodexUsage(
{ rate_limit: { primary_window: { ...primary, limit_window_seconds: seconds } } },
NOW,
)!;
expect(report).toContain(`${expected}: 75% remaining (25% used)`);
});
it("groups actual primary and secondary durations under their quota", () => {
const report = formatCodexUsage(
{
rate_limit: {
primary_window: { ...primary, limit_window_seconds: 18_000 },
secondary_window: { ...primary, used_percent: 10, limit_window_seconds: 604_800 },
},
},
NOW,
)!;
expect(report).toContain("Codex\n 5h window: 75% remaining (25% used)");
expect(report).toContain(" 7d window: 90% remaining (10% used)");
expect(report).not.toContain("Primary");
expect(report).not.toContain("Secondary");
});
it("uses supplied relative resets without guessing a window duration", () => {
@@ -75,11 +134,35 @@ it("uses supplied relative resets without guessing a window duration", () => {
},
NOW,
)!;
expect(report).toContain("Primary (duration unknown): 40% used · 60% remaining");
expect(report).toContain("2026-01-10 12:01:30 UTC (in 1m 30s)");
expect(report).toContain("Primary window (duration not reported): 60% remaining (40% used)");
expect(report).toContain("Jan 10, 2026, 12:01:30 UTC (in 1m 30s)");
});
it("reports unknown invalid reset times and never resets a measured balance locally", () => {
it.each([
["2026-01-10T12:00:00Z", "Jan 10, 2026, 08:00:00 EST"],
["2026-07-10T12:00:00Z", "Jul 10, 2026, 09:00:00 EDT"],
["2026-01-10T00:00:00Z", "Jan 9, 2026, 20:00:00 EST"],
])("uses local reset date, zone and relative time: %s", (now, expected) => {
timeZone = "America/New_York";
const report = formatCodexUsage(
{ rate_limit: { primary_window: { reset_after_seconds: 3600 } } },
Date.parse(now),
)!;
expect(report).toContain(`Resets: ${expected} (in 1h)`);
expect(report).toContain("remaining not reported");
expect(report).not.toContain("UTC");
});
it("prefers an absolute reset to a conflicting relative reset", () => {
const report = formatCodexUsage(
{ rate_limit: { primary_window: { ...primary, reset_after_seconds: 90 } } },
NOW,
)!;
expect(report).toContain("Jan 10, 2026, 13:00:00 UTC (in 1h)");
expect(report).not.toContain("in 1m 30s");
});
it("reports invalid resets as unknown and never renews a measured balance locally", () => {
const report = formatCodexUsage(
{
rate_limit: {
@@ -90,10 +173,32 @@ it("reports unknown invalid reset times and never resets a measured balance loca
},
NOW,
)!;
expect(report).toContain("100% used · 0% remaining");
expect(report).toContain("reset time passed; refresh usage");
expect(report).toContain("0% remaining (100% used)");
expect(report).toContain("Jan 10, 2026, 11:59:59 UTC (reset time passed; refresh usage)");
expect(report).toContain("Resets: unknown");
expect(report).toContain("Limit reached");
expect(report.indexOf("Codex: limit reached")).toBeLessThan(report.indexOf("\nCodex\n"));
expect(report).not.toContain("100% remaining");
});
it.each([
{},
{ reset_at: "1768050000" },
{ reset_at: 1e20, reset_after_seconds: 60 },
{ reset_after_seconds: -1 },
{ reset_after_seconds: "60" },
])("does not fabricate a reset from missing or invalid values: %j", (reset) => {
expect(
formatCodexUsage({ rate_limit: { primary_window: { used_percent: 25, ...reset } } }, NOW),
).toContain("Resets: unknown");
});
it("treats a reset exactly at now as expired without claiming renewal", () => {
const report = formatCodexUsage(
{ rate_limit: { primary_window: { used_percent: 100, reset_after_seconds: 0 } } },
NOW,
)!;
expect(report).toContain("0% remaining (100% used)");
expect(report).toContain("reset time passed; refresh usage");
});
it.each([
@@ -102,14 +207,25 @@ it.each([
[{ has_credits: false, balance: "0" }, "0 balance (unavailable)"],
[{ has_credits: true }, "available; balance not reported"],
[{ has_credits: false, balance: "0x10" }, "unavailable; balance not reported"],
[{ balance: 0 }, "0 balance (availability unknown)"],
[{ balance: -1 }, "availability unknown; balance not reported"],
[{ balance: Infinity }, "availability unknown; balance not reported"],
[{ balance: "1e3" }, "availability unknown; balance not reported"],
[{ balance: " 12.50 " }, "12.5 balance (availability unknown)"],
[{ unlimited: "true", has_credits: "true" }, "availability unknown; balance not reported"],
] as const)("reports only supplied credit facts: %j", (credits, expected) => {
expect(formatCodexUsage({ credits }, NOW)).toContain(`Credits: ${expected}`);
});
it("includes review and additional buckets plus explicit account restrictions", () => {
it("puts spending, account and all bucket restrictions before healthy quotas", () => {
const report = formatCodexUsage(
{
code_review_rate_limit: { primary_window: { ...primary, limit_window_seconds: 604_800 } },
rate_limit: { primary_window: primary },
code_review_rate_limit: {
limit_reached: true,
allowed: false,
primary_window: { ...primary, limit_window_seconds: 604_800 },
},
additional_rate_limits: [
{
metered_feature: "codex_other",
@@ -117,16 +233,60 @@ it("includes review and additional buckets plus explicit account restrictions",
rate_limit: { allowed: false, primary_window: primary },
},
],
credits: { has_credits: false, balance: "0" },
spend_control: { reached: true },
rate_limit_reached_type: { type: "workspace_owner_credits_depleted" },
},
NOW,
)!;
expect(report).toContain("Code review\n Primary (7d)");
expect(report).toContain("Other model pool\n Primary (30m)");
expect(report).toContain("Requests not currently allowed");
expect(report).toContain("Workspace spending limit: reached");
expect(report).toContain("Account limit: workspace owner credits depleted");
expect(report.startsWith("codex account usage · openai-codex\n")).toBe(true);
for (const warning of [
"Workspace spending limit: reached",
"Account limit: workspace owner credits depleted",
"Code review: limit reached; requests not currently allowed",
"Other model pool: requests not currently allowed",
"Credits: 0 balance (unavailable)",
]) {
expect(report).toContain(warning);
expect(report.indexOf(warning)).toBeLessThan(report.indexOf("\nCodex\n"));
}
expect(report.indexOf("Workspace spending limit")).toBeLessThan(report.indexOf("Code review:"));
expect(report).toContain("Code review\n 7d window: 75% remaining (25% used)");
expect(report).toContain("Other model pool\n 30m window: 75% remaining (25% used)");
});
it("keeps access restrictions visible even when all quota windows are absent", () => {
const report = formatCodexUsage(
{
rate_limit: { allowed: false },
code_review_rate_limit: { limit_reached: true },
additional_rate_limits: [{ metered_feature: "Extra", rate_limit: { allowed: false } }],
},
NOW,
)!;
expect(report).toBe(`codex account usage · openai-codex
Plan: unknown
Codex: requests not currently allowed
Code review: limit reached
Extra: requests not currently allowed
Not reported: Codex quota, Code review quota, Extra quota, credits.`);
});
it("does not interpret truthy restriction values as explicit restrictions", () => {
const report = formatCodexUsage(
{
rate_limit: { allowed: "false", limit_reached: "true" },
spend_control: { reached: "true" },
rate_limit_reached_type: { type: 123 },
},
NOW,
)!;
expect(report).not.toMatch(
/requests not currently allowed|limit reached|spending limit|Account limit/,
);
expect(report).toContain("Not reported: Codex quota, credits.");
});
it("bounds and sanitizes backend labels and additional-bucket output", () => {
@@ -146,6 +306,25 @@ it("bounds and sanitizes backend labels and additional-bucket output", () => {
expect(report.length).toBeLessThan(5000);
});
it("sanitizes restriction labels and retains additional-bucket name fallbacks", () => {
const report = formatCodexUsage(
{
rate_limit_reached_type: { type: "owner\r\ncredits\u001bdepleted" },
additional_rate_limits: [
{ limit_name: "\n", metered_feature: "Fallback\tpool", rate_limit: { allowed: false } },
null,
{ limit_name: 42, metered_feature: false },
],
},
NOW,
)!;
expect(report).toContain("Account limit: owner credits depleted");
expect(report).toContain("Fallback pool: requests not currently allowed");
expect(report).toContain(
"Not reported: Codex quota, Fallback pool quota, Additional 2 quota, Additional 3 quota, credits.",
);
});
it.each([null, [], {}, { unrelated: 10 }, { rate_limit: "wrong shape" }])(
"rejects unrecognized response shapes instead of fabricating healthy usage: %j",
(payload) => {
+56 -34
View File
@@ -20,20 +20,40 @@ export function formatCodexUsage(value: unknown, now = Date.now()): string | und
const sections = readUsageSections(value);
if (!sections) return undefined;
const rows = [
"codex account usage · openai-codex",
`Plan: ${sections.plan ?? "unknown"}`,
"",
...bucketRows("Codex", sections.codex, now),
];
const buckets = [{ name: "Codex", bucket: sections.codex }];
if (sections.includeCodeReview) {
rows.push("", ...bucketRows("Code review", sections.codeReview, now));
buckets.push({ name: "Code review", bucket: sections.codeReview });
}
rows.push(...additionalBucketRows(sections.additional, now));
rows.push("", `Credits: ${creditStatus(sections.credits)}`);
if (sections.spendingLimitReached) rows.push("Workspace spending limit: reached");
buckets.push(...additionalBuckets(sections.additional));
const rows = ["codex account usage · openai-codex", `Plan: ${sections.plan ?? "unknown"}`];
const warnings: string[] = [];
if (sections.spendingLimitReached) warnings.push("Workspace spending limit: reached");
if (sections.accountLimit)
rows.push(`Account limit: ${sections.accountLimit.replaceAll("_", " ")}`);
warnings.push(`Account limit: ${sections.accountLimit.replaceAll("_", " ")}`);
for (const { name, bucket } of buckets) {
const restrictions: string[] = [];
if (bucket?.limit_reached === true) restrictions.push("limit reached");
if (bucket?.allowed === false) restrictions.push("requests not currently allowed");
if (restrictions.length) warnings.push(`${name}: ${restrictions.join("; ")}`);
}
if (warnings.length) rows.push("", ...warnings);
if (sections.credits) rows.push(`Credits: ${creditStatus(sections.credits)}`);
const notReported: string[] = [];
for (const { name, bucket } of buckets) {
const quotaRows = bucketRows(name, bucket, now);
if (quotaRows.length) rows.push("", ...quotaRows);
else notReported.push(`${name} quota`);
}
if (!sections.credits) notReported.push("credits");
if (notReported.length) rows.push("", `Not reported: ${notReported.join(", ")}.`);
if (sections.additional && sections.additional.length > MAX_ADDITIONAL_BUCKETS) {
rows.push(
"",
`${sections.additional.length - MAX_ADDITIONAL_BUCKETS} additional entries omitted.`,
);
}
return rows.join("\n");
}
@@ -77,49 +97,42 @@ function readUsageSections(value: unknown): UsageSections | undefined {
}
function bucketRows(name: string, bucket: JsonObject | undefined, now: number): string[] {
if (!bucket) return [name, " Not reported"];
const rows = [
if (!bucket || (!readObject(bucket.primary_window) && !readObject(bucket.secondary_window)))
return [];
return [
name,
...windowRows("Primary", bucket.primary_window, now),
...windowRows("Secondary", bucket.secondary_window, now),
];
if (bucket.limit_reached === true) rows.push(" Limit reached");
else if (bucket.allowed === false) rows.push(" Requests not currently allowed");
return rows;
}
function windowRows(name: string, value: unknown, now: number): string[] {
const window = readObject(value);
if (!window) return [` ${name}: not reported`];
if (!window) return [` ${name} window: not reported`];
const seconds = finiteNonnegativeNumber(window.limit_window_seconds);
let interval = "duration unknown";
let label = `${name} window (duration not reported)`;
if (seconds !== undefined && Number.isSafeInteger(seconds) && seconds > 0)
interval = duration(seconds);
label = `${duration(seconds)} window`;
const used = finiteNonnegativeNumber(window.used_percent);
let usage = "usage unknown";
let usage = "remaining not reported";
if (used !== undefined && used <= 100) {
usage = `${NUMBERS.format(used)}% used · ${NUMBERS.format(100 - used)}% remaining`;
usage = `${NUMBERS.format(100 - used)}% remaining (${NUMBERS.format(used)}% used)`;
}
return [` ${name} (${interval}): ${usage}`, ` Resets: ${resetTime(window, now)}`];
return [` ${label}: ${usage}`, ` Resets: ${resetTime(window, now)}`];
}
function additionalBucketRows(additional: unknown[] | undefined, now: number): string[] {
if (!additional) return [];
const rows: string[] = [];
for (const [index, value] of additional.slice(0, MAX_ADDITIONAL_BUCKETS).entries()) {
function additionalBuckets(
additional: unknown[] | undefined,
): { name: string; bucket: JsonObject | undefined }[] {
return (additional ?? []).slice(0, MAX_ADDITIONAL_BUCKETS).map((value, index) => {
const entry = readObject(value);
const name =
sanitizeDisplayLabel(entry?.limit_name) ??
sanitizeDisplayLabel(entry?.metered_feature) ??
`Additional ${index + 1}`;
const quota = readObject(entry?.rate_limit);
rows.push("", ...bucketRows(name, quota, now));
}
if (additional.length > MAX_ADDITIONAL_BUCKETS) {
rows.push("", `${additional.length - MAX_ADDITIONAL_BUCKETS} additional entries omitted.`);
}
return rows;
return { name, bucket: readObject(entry?.rate_limit) };
});
}
function creditStatus(credits: JsonObject | undefined): string {
@@ -151,7 +164,16 @@ function resetTime(window: JsonObject, now: number): string {
else if (relative !== undefined) timestamp = now + relative * 1000;
const date = new Date(timestamp);
if (!Number.isFinite(date.getTime())) return "unknown";
const time = date.toISOString().replace("T", " ").replace(".000Z", " UTC");
const time = new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h23",
timeZoneName: "short",
}).format(date);
const remaining = Math.ceil((timestamp - now) / 1000);
return remaining > 0
? `${time} (in ${duration(remaining)})`
+39 -2
View File
@@ -6,9 +6,13 @@ import type {
import { expect, it, vi } from "vitest";
import { registerCommand } from "./commands.ts";
function setup(argumentActions?: readonly string[]) {
type Command = Parameters<ExtensionAPI["registerCommand"]>[1];
function setup(
argumentActions?: readonly string[],
getArgumentCompletions?: Command["getArgumentCompletions"],
) {
type Start = (event: unknown, ctx: ExtensionContext) => void | Promise<void>;
type Command = Parameters<ExtensionAPI["registerCommand"]>[1];
let start: Start | undefined;
let command: Command | undefined;
const handler = vi.fn();
@@ -29,6 +33,7 @@ function setup(argumentActions?: readonly string[]) {
actions: ["on", "off", "test", "toggle"],
defaultAction: "toggle",
argumentActions,
getArgumentCompletions,
handler,
});
if (!start) throw new Error("Missing session_start handler");
@@ -69,3 +74,35 @@ it("does not allow arguments for other actions or invent URL completions", async
expect(f.command.getArgumentCompletions?.("test https://")).toBe(null);
expect(f.command.getArgumentCompletions?.("TE")).toEqual([{ value: "test", label: "test" }]);
});
it("keeps the default action completions when no override is supplied", () => {
const { command } = setup();
expect(command.getArgumentCompletions?.(" O")).toEqual([
{ value: "on", label: "on" },
{ value: "off", label: "off" },
]);
expect(command.getArgumentCompletions?.("unknown")).toBe(null);
expect(command.getArgumentCompletions?.("on ")).toBe(null);
});
it("passes through a native completer without normalizing its input or replacing null", () => {
const complete = vi.fn(() => null);
const { command } = setup(undefined, complete);
expect(command.getArgumentCompletions).toBe(complete);
expect(command.getArgumentCompletions?.(" ON ")).toBe(null);
expect(complete).toHaveBeenCalledWith(" ON ");
});
it("supports asynchronous native completion items without changing strict routing", async () => {
const items = [{ value: "test extra", label: "extra", description: "Fixture argument" }];
const complete = vi.fn(async () => items);
const f = setup(["test"], complete);
expect(await f.command.getArgumentCompletions?.("test e")).toBe(items);
await f.command.handler("test extra", f.ctx);
expect(f.handler).toHaveBeenLastCalledWith("test", f.ctx, "extra");
f.handler.mockClear();
await f.command.handler("on extra", f.ctx);
await f.command.handler("unknown", f.ctx);
expect(f.handler).not.toHaveBeenCalled();
expect(f.notify).toHaveBeenCalledTimes(2);
});
+3 -1
View File
@@ -9,6 +9,7 @@ interface CommandOptions {
defaultAction: string;
/** Only listed actions accept trailing input; preserve it verbatim (for URLs, etc.). */
argumentActions?: readonly string[];
getArgumentCompletions?: NativeCommand["getArgumentCompletions"];
handler(action: string, ctx: ExtensionCommandContext, argument?: string): void | Promise<void>;
}
@@ -33,7 +34,8 @@ export function registerCommand(pi: ExtensionAPI, options: CommandOptions): void
function createCommand(options: CommandOptions): NativeCommand {
return {
description: `${options.description} — ${commandUsage(options)}`,
getArgumentCompletions: (prefix) => completeActions(options.actions, prefix),
getArgumentCompletions:
options.getArgumentCompletions ?? ((prefix) => completeActions(options.actions, prefix)),
handler: (args, ctx) => invokeCommand(options, args, ctx),
};
}