summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src/models/resolver.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-20 15:04:26 +0900
committerAdam Malczewski <[email protected]>2026-05-20 15:04:26 +0900
commitadc8bd185b54935e7a31aae04da3175b7989927a (patch)
tree031fa53009a4708ce0cc15c0e3dcb2f2a73cf2d9 /packages/core/src/models/resolver.ts
parent52af4ca33b1f83b8f863a6267d447944f55e729d (diff)
downloaddispatch-adc8bd185b54935e7a31aae04da3175b7989927a.tar.gz
dispatch-adc8bd185b54935e7a31aae04da3175b7989927a.zip
feat: phase 3 — config, skills, model groups, task list, and sidebar UI
- Config system: TOML-based dispatch.toml with hot-reload via chokidar - Model/key resolution: tag-based model selection, key fallback chains - Skills system: directory loader with TOML frontmatter, agent mappings - Task list tool: add/update/list/get operations with WebSocket events - API routes: GET /config, /skills, /skills/:name, /models, /models/resolve - Frontend: sidebar with model status, task list, config viewer, skills browser, permission log - Sliding sidebar animation using CSS transitions (not Svelte transitions)
Diffstat (limited to 'packages/core/src/models/resolver.ts')
-rw-r--r--packages/core/src/models/resolver.ts88
1 files changed, 88 insertions, 0 deletions
diff --git a/packages/core/src/models/resolver.ts b/packages/core/src/models/resolver.ts
new file mode 100644
index 0000000..3ada713
--- /dev/null
+++ b/packages/core/src/models/resolver.ts
@@ -0,0 +1,88 @@
+import type { ResolvedModel } from "../types/index.js";
+import type { ModelRegistry } from "./registry.js";
+
+export class ModelResolver {
+ private registry: ModelRegistry;
+
+ constructor(registry: ModelRegistry) {
+ this.registry = registry;
+ }
+
+ resolve(tag: string): ResolvedModel | null {
+ const models = this.registry.getModelsByTag(tag);
+ const keys = this.registry.getKeys();
+
+ for (const keyState of keys) {
+ if (keyState.status !== "active") continue;
+ const model = models.find((m) => m.provider === keyState.definition.provider);
+ if (model) {
+ return { model, key: keyState.definition };
+ }
+ }
+
+ return null;
+ }
+
+ async waitForKey(
+ tag: string,
+ options?: {
+ pollIntervalMs?: number;
+ signal?: AbortSignal;
+ onWaiting?: () => void;
+ onResume?: () => void;
+ },
+ ): Promise<ResolvedModel | null> {
+ const pollIntervalMs = options?.pollIntervalMs ?? 60000;
+ const signal = options?.signal;
+
+ // Try immediately first
+ const immediate = this.resolve(tag);
+ if (immediate) return immediate;
+
+ // Check if aborted before entering wait state
+ if (signal?.aborted) return null;
+
+ options?.onWaiting?.();
+
+ return new Promise<ResolvedModel | null>((resolve) => {
+ let timer: ReturnType<typeof setTimeout> | null = null;
+
+ const cleanup = () => {
+ if (timer !== null) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ };
+
+ const onAbort = () => {
+ cleanup();
+ resolve(null);
+ };
+
+ if (signal) {
+ signal.addEventListener("abort", onAbort, { once: true });
+ }
+
+ const poll = () => {
+ if (signal?.aborted) {
+ resolve(null);
+ return;
+ }
+
+ const result = this.resolve(tag);
+ if (result) {
+ if (signal) {
+ signal.removeEventListener("abort", onAbort);
+ }
+ options?.onResume?.();
+ resolve(result);
+ return;
+ }
+
+ timer = setTimeout(poll, pollIntervalMs);
+ };
+
+ timer = setTimeout(poll, pollIntervalMs);
+ });
+ }
+}