diff options
| author | Adam Malczewski <[email protected]> | 2026-06-06 18:55:53 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-06 18:55:53 +0900 |
| commit | 22936857685c318b71752d625808100b1a96e63e (patch) | |
| tree | 5e10a73d616c206e3820a8d8568e5f3d4c8a302e /packages/surface-loaded-extensions | |
| parent | 969afc45f895230fe3da1c737f18e64452efc8f2 (diff) | |
| download | dispatch-22936857685c318b71752d625808100b1a96e63e.tar.gz dispatch-22936857685c318b71752d625808100b1a96e63e.zip | |
feat(frontend,wire): surface system (FE slice 1) + @dispatch/wire types-only split (B2)
FE slice 1 — backend-declared, frontend-agnostic surface system (verified live): new types-only @dispatch/ui-contract (SurfaceSpec / field kinds / region / ActionRef / catalog), surface-registry (typed service handle), transport-ws (Bun WS :24205, path-agnostic upgrade), surface-loaded-extensions (first real surface); kernel HostAPI.getExtensions; host-bin wiring; bin/up. Harness: retire AGENTS 'backend only', ORCHESTRATOR §3/§7/§8, frontend-design.md locked.
B2 — wire-types split (chat-slice prerequisite): new types-only @dispatch/wire single-sources the wire ABI (AgentEvent + 11 variants; conversation model Chunk/ChatMessage/Role/TurnId/StepId + 6 chunk variants; Usage) with zero @dispatch/* deps. @dispatch/kernel re-exports via shims so its public surface is byte-identical (zero consumer blast radius). transport-contract re-exports AgentEvent from @dispatch/wire and drops its @dispatch/kernel dependency, so HTTP clients (the web frontend) consume the wire without the kernel runtime.
tsc -b + biome clean; 460 vitest + 77 bun pass.
Diffstat (limited to 'packages/surface-loaded-extensions')
| -rw-r--r-- | packages/surface-loaded-extensions/package.json | 13 | ||||
| -rw-r--r-- | packages/surface-loaded-extensions/src/extension.ts | 43 | ||||
| -rw-r--r-- | packages/surface-loaded-extensions/src/index.ts | 2 | ||||
| -rw-r--r-- | packages/surface-loaded-extensions/src/spec.test.ts | 94 | ||||
| -rw-r--r-- | packages/surface-loaded-extensions/src/spec.ts | 25 | ||||
| -rw-r--r-- | packages/surface-loaded-extensions/tsconfig.json | 10 |
6 files changed, 187 insertions, 0 deletions
diff --git a/packages/surface-loaded-extensions/package.json b/packages/surface-loaded-extensions/package.json new file mode 100644 index 0000000..66e2e69 --- /dev/null +++ b/packages/surface-loaded-extensions/package.json @@ -0,0 +1,13 @@ +{ + "name": "@dispatch/surface-loaded-extensions", + "version": "0.0.0", + "type": "module", + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "dependencies": { + "@dispatch/kernel": "workspace:*", + "@dispatch/ui-contract": "workspace:*", + "@dispatch/surface-registry": "workspace:*" + } +} diff --git a/packages/surface-loaded-extensions/src/extension.ts b/packages/surface-loaded-extensions/src/extension.ts new file mode 100644 index 0000000..abef4b6 --- /dev/null +++ b/packages/surface-loaded-extensions/src/extension.ts @@ -0,0 +1,43 @@ +import type { Extension, HostAPI, Manifest } from "@dispatch/kernel"; +import type { SurfaceProvider } from "@dispatch/surface-registry"; +import { surfaceRegistryHandle } from "@dispatch/surface-registry"; +import { buildLoadedExtensionsSpec } from "./spec.js"; + +export const manifest: Manifest = { + id: "surface-loaded-extensions", + name: "Loaded Extensions Surface", + version: "0.0.0", + apiVersion: "^0.1.0", + trust: "bundled", + activation: "eager", + dependsOn: ["surface-registry"], + contributes: { services: [] }, +}; + +export function createLoadedExtensionsExtension(): Extension { + let dispose: (() => void) | undefined; + + return { + manifest, + activate(host: HostAPI) { + const registry = host.getService(surfaceRegistryHandle); + + const provider: SurfaceProvider = { + catalogEntry: { + id: "loaded-extensions", + region: "side", + title: "Loaded Extensions", + }, + getSpec() { + return buildLoadedExtensionsSpec(host.getExtensions()); + }, + invoke() {}, + }; + + dispose = registry.register(provider); + }, + deactivate() { + dispose?.(); + }, + }; +} diff --git a/packages/surface-loaded-extensions/src/index.ts b/packages/surface-loaded-extensions/src/index.ts new file mode 100644 index 0000000..bc10dc5 --- /dev/null +++ b/packages/surface-loaded-extensions/src/index.ts @@ -0,0 +1,2 @@ +export { createLoadedExtensionsExtension, manifest } from "./extension.js"; +export { buildLoadedExtensionsSpec } from "./spec.js"; diff --git a/packages/surface-loaded-extensions/src/spec.test.ts b/packages/surface-loaded-extensions/src/spec.test.ts new file mode 100644 index 0000000..9c1aa6a --- /dev/null +++ b/packages/surface-loaded-extensions/src/spec.test.ts @@ -0,0 +1,94 @@ +import type { Manifest } from "@dispatch/kernel"; +import type { StatField } from "@dispatch/ui-contract"; +import { describe, expect, it } from "vitest"; +import { buildLoadedExtensionsSpec } from "./spec.js"; + +function fakeManifest(id: string, name: string, version: string): Manifest { + return { + id, + name, + version, + apiVersion: "^0.1.0", + trust: "bundled", + }; +} + +describe("buildLoadedExtensionsSpec", () => { + it("returns a count stat of '0' and no extension stats for empty manifests", () => { + const spec = buildLoadedExtensionsSpec([]); + + expect(spec.id).toBe("loaded-extensions"); + expect(spec.region).toBe("side"); + expect(spec.title).toBe("Loaded Extensions"); + expect(spec.fields).toHaveLength(1); + expect(spec.fields[0]).toEqual({ + kind: "stat", + label: "Loaded", + value: "0", + }); + }); + + it("returns a count stat plus one stat per manifest in order", () => { + const manifests = [ + fakeManifest("alpha", "Alpha", "1.0.0"), + fakeManifest("beta", "Beta", "2.3.1"), + fakeManifest("gamma", "Gamma", "0.5.0"), + ]; + + const spec = buildLoadedExtensionsSpec(manifests); + + expect(spec.fields).toHaveLength(4); + expect(spec.fields[0]).toEqual({ + kind: "stat", + label: "Loaded", + value: "3", + }); + expect(spec.fields[1]).toEqual({ + kind: "stat", + label: "Alpha", + value: "1.0.0", + }); + expect(spec.fields[2]).toEqual({ + kind: "stat", + label: "Beta", + value: "2.3.1", + }); + expect(spec.fields[3]).toEqual({ + kind: "stat", + label: "Gamma", + value: "0.5.0", + }); + }); + + it("preserves input order of manifests", () => { + const manifests = [ + fakeManifest("z-last", "Z Last", "1.0.0"), + fakeManifest("a-first", "A First", "2.0.0"), + ]; + + const spec = buildLoadedExtensionsSpec(manifests); + + expect((spec.fields[1] as StatField).label).toBe("Z Last"); + expect((spec.fields[2] as StatField).label).toBe("A First"); + }); + + it("sets the surface id, region, and title correctly", () => { + const spec = buildLoadedExtensionsSpec([]); + + expect(spec.id).toBe("loaded-extensions"); + expect(spec.region).toBe("side"); + expect(spec.title).toBe("Loaded Extensions"); + }); + + it("uses manifest.name as label and manifest.version as value", () => { + const manifests = [fakeManifest("my-ext", "My Extension", "3.2.1")]; + + const spec = buildLoadedExtensionsSpec(manifests); + + expect(spec.fields[1]).toEqual({ + kind: "stat", + label: "My Extension", + value: "3.2.1", + }); + }); +}); diff --git a/packages/surface-loaded-extensions/src/spec.ts b/packages/surface-loaded-extensions/src/spec.ts new file mode 100644 index 0000000..bd3dd56 --- /dev/null +++ b/packages/surface-loaded-extensions/src/spec.ts @@ -0,0 +1,25 @@ +import type { Manifest } from "@dispatch/kernel"; +import type { StatField, SurfaceSpec } from "@dispatch/ui-contract"; + +/** + * Pure core — builds the SurfaceSpec for the loaded-extensions surface. + * Zero I/O, zero ambient state. Decision logic only: input → output. + */ +export function buildLoadedExtensionsSpec(manifests: readonly Manifest[]): SurfaceSpec { + const fields: StatField[] = [{ kind: "stat", label: "Loaded", value: String(manifests.length) }]; + + for (const manifest of manifests) { + fields.push({ + kind: "stat", + label: manifest.name, + value: manifest.version, + }); + } + + return { + id: "loaded-extensions", + region: "side", + title: "Loaded Extensions", + fields, + }; +} diff --git a/packages/surface-loaded-extensions/tsconfig.json b/packages/surface-loaded-extensions/tsconfig.json new file mode 100644 index 0000000..db257d0 --- /dev/null +++ b/packages/surface-loaded-extensions/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "rootDir": "src", "outDir": "dist", "composite": true }, + "include": ["src/**/*.ts"], + "references": [ + { "path": "../kernel" }, + { "path": "../ui-contract" }, + { "path": "../surface-registry" } + ] +} |
