feat: multi-work agent #12

Merged
StanPonomarev merged 5 commits from feat/multi-work into main 2026-09-18 10:28:03 +02:00
4 changed files with 51 additions and 28 deletions
Showing only changes of commit df61d9a282 - Show all commits
+1 -5
View File
@@ -237,11 +237,7 @@ export class QuotaTracker {
) {}
async init() {
try {
await this.resolveCredential();
} catch {
return;
}
await this.refresh();
}
private async resolveCredential(generation = this.generation) {
+10 -5
View File
@@ -60,9 +60,9 @@ export default Plugin.define({
setRevision((value) => value + 1);
});
const Slot = (props: { sessionID: string }) => {
const Slot = (props: { instance: string }) => {
const [status] = createResource(
() => `${props.sessionID}:${revision()}`,
() => `${props.instance}:${revision()}`,
() => loadQuotaStatus(() => quota.status({})),
);
return (
@@ -83,12 +83,17 @@ export default Plugin.define({
);
};
const remove = context.ui.slot({
const removeHome = context.ui.slot({
append: "home.footer.status",
render: () => <Slot instance="home" />,
});
const removeSidebar = context.ui.slot({
append: "sidebar.footer",
render: (input) => <Slot sessionID={input.sessionID} />,
render: (input) => <Slot instance={input.sessionID} />,
});
return () => {
remove();
removeHome();
removeSidebar();
unsubscribe();
clearInterval(retry);
};
+9 -9
View File
@@ -329,11 +329,11 @@ test("reads old cached snapshots without session estimates and removes excluded
]);
expect(status?.windows[0]).not.toHaveProperty("sessionConsumed");
expect(result.stored.get("chatgpt-quota")).not.toHaveProperty("sessions");
expect(result.calls()).toBe(0);
expect(result.calls()).toBe(1);
await result.tracker.dispose();
});
test("activates only for OAuth and refreshes quota at request start", async () => {
test("activates only for OAuth and refreshes quota at initialization and request start", async () => {
const key = fixture({ type: "key", key: "secret" }, [plus]);
await key.tracker.init();
await key.tracker.start("root");
@@ -360,10 +360,11 @@ test("activates only for OAuth and refreshes quota at request start", async () =
],
);
await oauth.tracker.init();
await oauth.tracker.start("root");
expect(oauth.calls()).toBe(1);
expect(oauth.tracker.status()?.windows[0]?.remaining).toBe(87.5);
await oauth.tracker.start("root");
expect(oauth.calls()).toBe(2);
expect(oauth.headers[0]?.get("chatgpt-account-id")).toBe("account-one");
await oauth.tracker.refresh();
const status = oauth.tracker.status();
expect(status?.windows[0]?.remaining).toBe(82.5);
expect(status?.windows[0]).not.toHaveProperty("sessionConsumed");
@@ -482,10 +483,10 @@ test("request completion refreshes once across terminal and idle events", async
await result.tracker.init();
await result.tracker.start("root");
await result.tracker.complete("root");
expect(result.tracker.status()?.windows[0]?.remaining).toBe(89);
expect(result.tracker.status()?.windows[0]?.remaining).toBe(70);
await result.tracker.complete("root");
expect(result.calls()).toBe(2);
expect(result.tracker.status()?.windows[0]?.remaining).toBe(89);
expect(result.calls()).toBe(3);
expect(result.tracker.status()?.windows[0]?.remaining).toBe(70);
await result.tracker.dispose();
});
@@ -513,8 +514,7 @@ test("an auth switch detaches an in-flight old-account response and immediately
},
],
);
await result.tracker.init();
const old = result.tracker.start("root");
const old = result.tracker.init();
while (result.calls() === 0) await Bun.sleep(1);
result.switch({
type: "oauth",
+31 -9
View File
@@ -5,7 +5,8 @@ import { createSignal } from "solid-js";
import { build } from "../scripts/build.js";
import type { QuotaStatus } from "../src/quota.js";
type SlotRender = (input: { readonly sessionID: string }) => JSX.Element;
type SidebarSlotRender = (input: { readonly sessionID: string }) => JSX.Element;
type HomeSlotRender = (input: Record<string, never>) => JSX.Element;
const status = (plan: string, remaining: number): QuotaStatus => ({
plan,
@@ -30,9 +31,13 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
);
const { default: plugin } = tui;
const pending = Promise.withResolvers<QuotaStatus | null>();
const responses: Array<() => Promise<QuotaStatus | null>> = [() => pending.promise];
const responses: Array<() => Promise<QuotaStatus | null>> = [
async () => status("home", 90),
() => pending.promise,
];
let statusCalls = 0;
let slotRender: SlotRender | undefined;
let sidebarRender: SidebarSlotRender | undefined;
let homeRender: HomeSlotRender | undefined;
let notifyUpdated: (() => void) | undefined;
const quota = {
@@ -52,8 +57,17 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
const contextFixture = {
client: { rpc: () => quota },
ui: {
slot(claim: { render: SlotRender }) {
slotRender = claim.render;
slot(claim: {
append: "home.footer.status" | "sidebar.footer";
render: HomeSlotRender | SidebarSlotRender;
}) {
if (claim.append === "home.footer.status") {
// SAFETY: The home slot renderer receives the empty home slot input.
homeRender = claim.render as HomeSlotRender;
} else {
// SAFETY: The only other fixture path is the session-aware sidebar slot.
sidebarRender = claim.render as SidebarSlotRender;
}
return () => undefined;
},
},
@@ -63,10 +77,18 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
// members implemented by this focused rendering fixture.
const context = Object.assign({} as Plugin.Context, contextFixture);
const cleanup = await plugin.setup(context);
if (!slotRender || !notifyUpdated) throw new Error("Plugin did not register its slot");
if (!sidebarRender || !homeRender || !notifyUpdated)
throw new Error("Plugin did not register its slots");
const renderHome = homeRender;
const home = await testRender(() => renderHome({}), { width: 70, height: 5 });
await home.waitForFrame((frame) => frame.includes("ChatGPT quota (home)"));
expect(home.captureCharFrame()).toContain("5h: 90% · due");
expect(statusCalls).toBe(1);
home.renderer.destroy();
const [sessionID, setSessionID] = createSignal("session-one");
const renderSlot = slotRender;
const renderSlot = sidebarRender;
const setup = await testRender(
() =>
renderSlot({
@@ -94,13 +116,13 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
throw new Error("disconnected");
});
notifyUpdated();
await setup.waitFor(() => statusCalls === 3);
await setup.waitFor(() => statusCalls === 4);
expect(setup.captureCharFrame()).not.toContain("ChatGPT quota");
responses.push(async () => status("pro", 42));
setSessionID("session-two");
await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (pro)"));
expect(statusCalls).toBe(4);
expect(statusCalls).toBe(5);
} finally {
setup.renderer.destroy();
if (cleanup) await cleanup();