diff options
| author | Adam Malczewski <[email protected]> | 2026-05-20 15:04:26 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-20 15:04:26 +0900 |
| commit | adc8bd185b54935e7a31aae04da3175b7989927a (patch) | |
| tree | 031fa53009a4708ce0cc15c0e3dcb2f2a73cf2d9 /packages/core/src/models/resolver.ts | |
| parent | 52af4ca33b1f83b8f863a6267d447944f55e729d (diff) | |
| download | dispatch-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.ts | 88 |
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); + }); + } +} |
