summaryrefslogtreecommitdiffhomepage
path: root/packages/cache-warming/src/extension.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-11 13:08:38 +0900
committerAdam Malczewski <[email protected]>2026-06-11 13:08:38 +0900
commitffbbcf692a97ec8648af39353b49f32896367207 (patch)
tree2e2ddfd03d4a868f4a4ba12e20586cc03c37f90a /packages/cache-warming/src/extension.ts
parent27fd0be36b2f6395249de5aacc86e41fe4e0207f (diff)
downloaddispatch-ffbbcf692a97ec8648af39353b49f32896367207.tar.gz
dispatch-ffbbcf692a97ec8648af39353b49f32896367207.zip
feat(surfaces): NumberField + per-conversation surface scoping; cache-warming controls
Extend the surface framework so cache-warming exposes per-conversation controls: - ui-contract: add NumberField (settable free-value numeric) to SurfaceField; add optional conversationId to subscribe/unsubscribe/invoke + surface/update - surface-registry: SurfaceContext { conversationId? } on getSpec/invoke (backward-compatible) - transport-ws: thread conversationId; key subscriptions by (surfaceId, conversationId); tag surface/update replies with conversationId - cache-warming: per-conversation surface — Toggle(enabled) + Number(interval seconds, cache-warming/set-interval) + Stat(last cache %); drop the currentConversationId closure Global surfaces (surface-loaded-extensions) unchanged. 784 vitest + 109 bun = 893 tests; tsc -b EXIT 0; biome clean.
Diffstat (limited to 'packages/cache-warming/src/extension.ts')
-rw-r--r--packages/cache-warming/src/extension.ts99
1 files changed, 49 insertions, 50 deletions
diff --git a/packages/cache-warming/src/extension.ts b/packages/cache-warming/src/extension.ts
index 16515a8..26d429b 100644
--- a/packages/cache-warming/src/extension.ts
+++ b/packages/cache-warming/src/extension.ts
@@ -1,8 +1,14 @@
import type { Extension, HostAPI, Manifest } from "@dispatch/kernel";
import { cacheWarmHandle, turnSettled, turnStarted } from "@dispatch/session-orchestrator";
-import type { SurfaceProvider } from "@dispatch/surface-registry";
+import type { SurfaceContext, SurfaceProvider } from "@dispatch/surface-registry";
import { surfaceRegistryHandle } from "@dispatch/surface-registry";
import type { SurfaceSpec } from "@dispatch/ui-contract";
+import {
+ buildConversationSpec,
+ buildDefaultSpec,
+ parseIntervalPayload,
+ secondsToMs,
+} from "./pure.js";
import { createCacheWarmer } from "./warmer.js";
export const manifest: Manifest = {
@@ -19,41 +25,13 @@ export const manifest: Manifest = {
},
};
-function buildSurfaceSpec(
- _conversationId: string | undefined,
- enabled: boolean,
- lastPct: number | null,
-): SurfaceSpec {
- const pctDisplay = lastPct === null ? "—" : `${lastPct}%`;
- return {
- id: "cache-warming",
- region: "side",
- title: "Cache Warming",
- fields: [
- {
- kind: "toggle",
- label: "Enabled",
- value: enabled,
- action: { actionId: "cache-warming/toggle" },
- },
- {
- kind: "stat",
- label: "Last Cache %",
- value: pctDisplay,
- },
- ],
- };
-}
-
export function activate(host: HostAPI): void {
const warmService = host.getService(cacheWarmHandle);
const registry = host.getService(surfaceRegistryHandle);
const storage = host.storage("cache-warming");
- let currentConversationId: string | undefined;
+ const subscribers = new Set<() => void>();
- // Timer wrapper: setTimeout/clearTimeout return Timeout in Node types,
- // but our TimerDeps uses number ids. Map between them.
const timeoutMap = new Map<number, ReturnType<typeof setTimeout>>();
let nextTimerId = 1;
@@ -76,46 +54,67 @@ export function activate(host: HostAPI): void {
},
},
onSurfaceChange: () => {
- // Surface subscribers will re-fetch on next getSpec()
+ for (const notify of subscribers) {
+ notify();
+ }
},
});
host.on(turnStarted, (payload) => {
- currentConversationId = payload.conversationId;
warmer.onTurnStarted(payload.conversationId);
});
host.on(turnSettled, (payload) => {
- currentConversationId = payload.conversationId;
warmer.onTurnSettled(payload.conversationId, {
...(payload.cwd !== undefined ? { cwd: payload.cwd } : {}),
...(payload.modelName !== undefined ? { modelName: payload.modelName } : {}),
});
});
+ function getSpec(context?: SurfaceContext): SurfaceSpec {
+ const convId = context?.conversationId;
+ if (convId === undefined) {
+ return buildDefaultSpec();
+ }
+ const state = warmer.getState(convId);
+ return buildConversationSpec(state.enabled, state.intervalMs, state.lastPct);
+ }
+
+ async function invoke(
+ actionId: string,
+ payload?: unknown,
+ context?: SurfaceContext,
+ ): Promise<void> {
+ const convId = context?.conversationId;
+ if (convId === undefined) return;
+
+ if (actionId === "cache-warming/toggle") {
+ const current = warmer.getState(convId);
+ await warmer.setEnabled(convId, !current.enabled);
+ }
+
+ if (actionId === "cache-warming/set-interval") {
+ const seconds = parseIntervalPayload(payload);
+ if (seconds === null) return;
+ const ms = secondsToMs(seconds);
+ if (ms === null) return;
+ await warmer.setIntervalMs(convId, ms);
+ }
+ }
+
const provider: SurfaceProvider = {
catalogEntry: {
id: "cache-warming",
region: "side",
title: "Cache Warming",
},
- getSpec() {
- const convId = currentConversationId;
- const state =
- convId !== undefined ? warmer.getState(convId) : { enabled: true, lastPct: null };
- return buildSurfaceSpec(convId, state.enabled, state.lastPct);
- },
- async invoke(actionId, payload) {
- const pl = payload as Record<string, unknown> | undefined;
- const convId =
- (typeof pl?.conversationId === "string" ? pl.conversationId : undefined) ??
- currentConversationId;
- if (convId === undefined) return;
-
- if (actionId === "cache-warming/toggle") {
- const current = warmer.getState(convId);
- await warmer.setEnabled(convId, !current.enabled);
- }
+ getSpec,
+ invoke,
+ subscribe(onChange) {
+ subscribers.add(onChange);
+ return () => {
+ subscribers.delete(onChange);
+ };
},
};