diff options
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/store.svelte.ts | 7 | ||||
| -rw-r--r-- | src/app/store.test.ts | 48 | ||||
| -rw-r--r-- | src/app/views/ChatPane.svelte | 8 |
3 files changed, 62 insertions, 1 deletions
diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts index a3bc226..fafd4d7 100644 --- a/src/app/store.svelte.ts +++ b/src/app/store.svelte.ts @@ -1474,6 +1474,8 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { const model = conv.model ?? activeModel; const store = createChatFor(conv.id, model, conv.workspaceId); chatStores.set(conv.id, store); + // Seed the cache timer's turn gap (only an idle conversation is waiting on the user). + if (conv.status === "idle") store.noteLastActivity(conv.lastActivityAt); void store.load(); subscribeChat(conv.id); tabsStore.openTab({ @@ -1492,6 +1494,11 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { // authoritative conversation record is the source of truth, so a // differing tab workspace is corrected here. tabsStore.setTitle(conv.id, conv.title); + // Seed the cache timer's turn gap from the backend (a tab restored + // from localStorage at boot has no live `done` to go by). Only an + // idle conversation is waiting on the user. + if (conv.status === "idle") + chatStores.get(conv.id)?.noteLastActivity(conv.lastActivityAt); const tab = tabsStore.tabs.find((t) => t.conversationId === conv.id); if (tab?.workspaceId !== conv.workspaceId) { tabsStore.setWorkspaceId(conv.id, conv.workspaceId); 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({ diff --git a/src/app/views/ChatPane.svelte b/src/app/views/ChatPane.svelte index 636cd7c..31e719e 100644 --- a/src/app/views/ChatPane.svelte +++ b/src/app/views/ChatPane.svelte @@ -7,13 +7,18 @@ * preservation, the ScrollToBottom affordance, and the empty-state * watermark. Keyed on the active conversation exactly like today's ChatView. */ - import { ChatView } from "../../features/chat"; + import { ChatView, isClaudeSubscriptionModel } from "../../features/chat"; import { createSmartScrollController, ScrollToBottom } from "../../features/smart-scroll"; import { preserveViewportAcross, type AppViewPorts } from "../view-ports"; let { ports }: { ports: AppViewPorts } = $props(); const store = $derived(ports.store); + // Cache timer: Claude Code subscription models only (5-minute prompt-cache expiry). + const cacheTimer = $derived( + isClaudeSubscriptionModel(store.activeModel) ? store.activeChat.cacheTimer : null, + ); + // One controller owns the chat scroll region; the effects below feed it the // edges (identical to the App.svelte wiring this moved out of). const smartScroll = createSmartScrollController(); @@ -81,6 +86,7 @@ thinkingKeyBase={store.activeChat.thinkingKeyBase} providerRetry={store.activeChat.providerRetry} apiBaseUrl={store.httpBase} + {cacheTimer} /> {/key} </div> |
