diff options
| author | Adam Malczewski <[email protected]> | 2026-10-19 17:02:40 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-10-19 17:02:40 +0900 |
| commit | 01f9fb2a4ad6110def531ead16b370c818e8ee81 (patch) | |
| tree | 398996139295c24350714bb02a88b0dc07545123 /src/app/store.test.ts | |
| parent | c4397f633415a46852f0e3b49c78ab7b2d87d4c1 (diff) | |
| download | dispatch-web-01f9fb2a4ad6110def531ead16b370c818e8ee81.tar.gz dispatch-web-01f9fb2a4ad6110def531ead16b370c818e8ee81.zip | |
feat(chat): cache timer for Claude Code subscription models
Per-step and per-turn timers for the gaps between model requests (the
Claude prompt cache expires after 5 min idle). A step gap runs live under
the executing tools from step-complete until its last tool result (or the
next step's output), then is stamped on that step's metrics line beside
its cache badge. A turn gap runs live on the turn's metrics line from
done/error until the next message is sent, then is stamped there. Counts
m:ss up to 10 min, then '10+ min'; colour tracks the 5-min TTL.
Gated on claude/<model>. Pure reducer in core/metrics/cache-timer.ts;
the turn gap is seeded from an idle conversation's lastActivityAt so it
survives a reload. Stamps are in-memory only.
Diffstat (limited to 'src/app/store.test.ts')
| -rw-r--r-- | src/app/store.test.ts | 48 |
1 files changed, 48 insertions, 0 deletions
diff --git a/src/app/store.test.ts b/src/app/store.test.ts index 89e418e..675fa64 100644 --- a/src/app/store.test.ts +++ b/src/app/store.test.ts @@ -855,6 +855,54 @@ describe("createAppStore", () => { store.dispose(); }); + it("fetchOpenConversations seeds the chat store's cache-timer turn gap from an idle conversation's lastActivityAt", async () => { + const ws = fakeSocket(); + const fetchImpl: typeof fetch = async (input: string | URL | Request): Promise<Response> => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + if (url.includes("/conversations?status=")) { + return new Response( + JSON.stringify({ + conversations: [ + { + id: "conv-seed", + createdAt: 1000, + lastActivityAt: 7777, + title: "Seed conv", + status: "idle", + workspaceId: "default", + model: "claude/claude-opus-4-6", + }, + ], + }), + { status: 200 }, + ); + } + if (url.endsWith("/metrics")) { + return new Response(JSON.stringify({ turns: [] }), { status: 200 }); + } + return new Response(JSON.stringify({ chunks: [], latestSeq: 0 }), { status: 200 }); + }; + + const store = createAppStore({ + socketFactory: () => ws, + fetchImpl, + localStorage: createFakeStorage(), + }); + ws.resolveOpen(); + + await vi.waitFor(() => { + expect(store.tabs.some((t) => t.conversationId === "conv-seed")).toBe(true); + }); + store.selectTab("conv-seed"); + expect(store.activeChat.cacheTimer.running).toEqual({ + kind: "turn", + turnId: null, + startedAt: 7777, + }); + + store.dispose(); + }); + it("conversation.open WS message carrying a model stamps the tab with it", async () => { const ws = fakeSocket(); const store = createAppStore({ |
