fix(codex): make controls and usage easier to navigate #5
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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
@@ -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?.();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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.");
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
@@ -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");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)})`
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user