This commit is contained in:
+20
-9
@@ -60,32 +60,43 @@ export default Plugin.define({
|
|||||||
setRevision((value) => value + 1);
|
setRevision((value) => value + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
const Slot = (props: { instance: string }) => {
|
const Slot = (props: { instance: string; centered?: boolean; visible?: () => boolean }) => {
|
||||||
const [status] = createResource(
|
const [status] = createResource(
|
||||||
() => `${props.instance}:${revision()}`,
|
() => (props.visible?.() === false ? false : `${props.instance}:${revision()}`),
|
||||||
() => loadQuotaStatus(() => quota.status({})),
|
() => loadQuotaStatus(() => quota.status({})),
|
||||||
);
|
);
|
||||||
return (
|
const Content = () => (
|
||||||
<Show when={status()}>
|
|
||||||
<box flexDirection="column">
|
<box flexDirection="column">
|
||||||
<text fg={context.theme.text.default}>
|
<text fg={context.theme.text.default}>
|
||||||
<b>ChatGPT quota ({status()!.plan})</b>
|
<b>ChatGPT quota ({status()!.plan})</b>
|
||||||
</text>
|
</text>
|
||||||
<For each={status()!.windows}>
|
<For each={status()!.windows}>
|
||||||
{(window) => (
|
{(window) => (
|
||||||
<text fg={context.theme.text.muted}>
|
<text fg={context.theme.text.muted}>{formatQuotaWindow(window)}</text>
|
||||||
{formatQuotaWindow(window)}
|
|
||||||
</text>
|
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</box>
|
</box>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Show when={status()}>
|
||||||
|
<Show when={props.centered} fallback={<Content />}>
|
||||||
|
<box width="100%" alignItems="center">
|
||||||
|
<Content />
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeHome = context.ui.slot({
|
const removeHome = context.ui.slot({
|
||||||
append: "home.footer.status",
|
after: "prompt.footer",
|
||||||
render: () => <Slot instance="home" />,
|
render: () => (
|
||||||
|
<Slot
|
||||||
|
instance="home"
|
||||||
|
centered
|
||||||
|
visible={() => context.ui.router.current().type === "home"}
|
||||||
|
/>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
const removeSidebar = context.ui.slot({
|
const removeSidebar = context.ui.slot({
|
||||||
append: "sidebar.footer",
|
append: "sidebar.footer",
|
||||||
|
|||||||
+30
-6
@@ -1,12 +1,13 @@
|
|||||||
import { expect, test } from "bun:test";
|
import { expect, test } from "bun:test";
|
||||||
import type { Plugin } from "@opencode/plugin/tui";
|
import type { Plugin } from "@opencode/plugin/tui";
|
||||||
|
import type { Route, SlotMap } from "@opencode/plugin/tui/context";
|
||||||
import { testRender, type JSX } from "@opentui/solid";
|
import { testRender, type JSX } from "@opentui/solid";
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
import { build } from "../scripts/build.js";
|
import { build } from "../scripts/build.js";
|
||||||
import type { QuotaStatus } from "../src/quota.js";
|
import type { QuotaStatus } from "../src/quota.js";
|
||||||
|
|
||||||
type SidebarSlotRender = (input: { readonly sessionID: string }) => JSX.Element;
|
type SidebarSlotRender = (input: { readonly sessionID: string }) => JSX.Element;
|
||||||
type HomeSlotRender = (input: Record<string, never>) => JSX.Element;
|
type HomeSlotRender = (input: SlotMap["prompt.footer"]) => JSX.Element;
|
||||||
|
|
||||||
const status = (plan: string, remaining: number): QuotaStatus => ({
|
const status = (plan: string, remaining: number): QuotaStatus => ({
|
||||||
plan,
|
plan,
|
||||||
@@ -39,6 +40,7 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
|
|||||||
let sidebarRender: SidebarSlotRender | undefined;
|
let sidebarRender: SidebarSlotRender | undefined;
|
||||||
let homeRender: HomeSlotRender | undefined;
|
let homeRender: HomeSlotRender | undefined;
|
||||||
let notifyUpdated: (() => void) | undefined;
|
let notifyUpdated: (() => void) | undefined;
|
||||||
|
const [route, setRoute] = createSignal<Route>({ type: "home" });
|
||||||
|
|
||||||
const quota = {
|
const quota = {
|
||||||
status() {
|
status() {
|
||||||
@@ -58,11 +60,12 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
|
|||||||
client: { rpc: () => quota },
|
client: { rpc: () => quota },
|
||||||
ui: {
|
ui: {
|
||||||
slot(claim: {
|
slot(claim: {
|
||||||
append: "home.footer.status" | "sidebar.footer";
|
after?: "prompt.footer";
|
||||||
|
append?: "sidebar.footer";
|
||||||
render: HomeSlotRender | SidebarSlotRender;
|
render: HomeSlotRender | SidebarSlotRender;
|
||||||
}) {
|
}) {
|
||||||
if (claim.append === "home.footer.status") {
|
if (claim.after === "prompt.footer") {
|
||||||
// SAFETY: The home slot renderer receives the empty home slot input.
|
// SAFETY: The prompt footer renderer receives the matching slot input.
|
||||||
homeRender = claim.render as HomeSlotRender;
|
homeRender = claim.render as HomeSlotRender;
|
||||||
} else {
|
} else {
|
||||||
// SAFETY: The only other fixture path is the session-aware sidebar slot.
|
// SAFETY: The only other fixture path is the session-aware sidebar slot.
|
||||||
@@ -70,6 +73,7 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
|
|||||||
}
|
}
|
||||||
return () => undefined;
|
return () => undefined;
|
||||||
},
|
},
|
||||||
|
router: { current: route },
|
||||||
},
|
},
|
||||||
theme: { text: { default: "#ffffff", muted: "#888888" } },
|
theme: { text: { default: "#ffffff", muted: "#888888" } },
|
||||||
};
|
};
|
||||||
@@ -81,12 +85,32 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
|
|||||||
throw new Error("Plugin did not register its slots");
|
throw new Error("Plugin did not register its slots");
|
||||||
|
|
||||||
const renderHome = homeRender;
|
const renderHome = homeRender;
|
||||||
const home = await testRender(() => renderHome({}), { width: 70, height: 5 });
|
const home = await testRender(() => renderHome({ mode: "normal", showDetails: false }), {
|
||||||
|
width: 40,
|
||||||
|
height: 5,
|
||||||
|
});
|
||||||
await home.waitForFrame((frame) => frame.includes("ChatGPT quota (home)"));
|
await home.waitForFrame((frame) => frame.includes("ChatGPT quota (home)"));
|
||||||
expect(home.captureCharFrame()).toContain("5h: 90% · due");
|
const homeFrame = home.captureCharFrame();
|
||||||
|
expect(homeFrame).toContain(" ChatGPT quota (home)");
|
||||||
|
expect(homeFrame).toContain(" 5h: 90% · due");
|
||||||
expect(statusCalls).toBe(1);
|
expect(statusCalls).toBe(1);
|
||||||
home.renderer.destroy();
|
home.renderer.destroy();
|
||||||
|
|
||||||
|
setRoute({ type: "session", sessionID: "session-one" });
|
||||||
|
const sessionPrompt = await testRender(
|
||||||
|
() =>
|
||||||
|
renderHome({
|
||||||
|
sessionID: "session-one",
|
||||||
|
mode: "normal",
|
||||||
|
showDetails: false,
|
||||||
|
}),
|
||||||
|
{ width: 40, height: 5 },
|
||||||
|
);
|
||||||
|
await sessionPrompt.renderOnce();
|
||||||
|
expect(sessionPrompt.captureCharFrame()).not.toContain("ChatGPT quota");
|
||||||
|
expect(statusCalls).toBe(1);
|
||||||
|
sessionPrompt.renderer.destroy();
|
||||||
|
|
||||||
const [sessionID, setSessionID] = createSignal("session-one");
|
const [sessionID, setSessionID] = createSignal("session-one");
|
||||||
const renderSlot = sidebarRender;
|
const renderSlot = sidebarRender;
|
||||||
const setup = await testRender(
|
const setup = await testRender(
|
||||||
|
|||||||
Reference in New Issue
Block a user