summaryrefslogtreecommitdiffhomepage
path: root/packages/todo/src/extension.ts
blob: 80af7aafe89a8ecd7f91cb4de6f86f7cdc99b196 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
 * todo extension — owns the per-conversation task list (state + surface + the
 * `todo_write` tool). Plugs into the kernel host via a manifest +
 * `activate(host)`; the model maintains the list via the tool, and the surface
 * renders the live list for the frontend (subscriber-notify, same pattern as
 * message-queue). State is in-memory (per-process, per-conversation) — no
 * persistence, mirroring message-queue.
 */

import type { Extension, HostAPI, Manifest } from "@dispatch/kernel";
import type { SurfaceContext, SurfaceProvider } from "@dispatch/surface-registry";
import { surfaceRegistryHandle } from "@dispatch/surface-registry";
import type { SurfaceSpec } from "@dispatch/ui-contract";
import { buildTodoSpec, getTodos, TODO_SURFACE_ID, type TodoState } from "./pure.js";
import { createTodoWriteTool } from "./tool.js";

export const manifest: Manifest = {
	id: "todo",
	name: "Todo Tool",
	version: "0.0.0",
	apiVersion: "^0.1.0",
	trust: "bundled",
	activation: "eager",
	dependsOn: ["surface-registry"],
	capabilities: {},
	contributes: {
		tools: ["todo_write"],
	},
};

export function activate(host: HostAPI): void {
	const registry = host.getService(surfaceRegistryHandle);

	const state: TodoState = new Map();
	const subscribers = new Set<() => void>();

	function notify(): void {
		for (const sub of subscribers) {
			sub();
		}
	}

	host.defineTool(createTodoWriteTool({ state, notify }));

	function getSpec(context?: SurfaceContext): SurfaceSpec {
		const convId = context?.conversationId;
		const todos = convId === undefined ? [] : getTodos(state, convId);
		return buildTodoSpec(todos);
	}

	function invoke(_actionId: string, _payload?: unknown, _context?: SurfaceContext): void {
		// The todo surface is read-only: the model mutates the list via the
		// `todo_write` tool; no client-facing surface actions.
	}

	const provider: SurfaceProvider = {
		catalogEntry: {
			id: TODO_SURFACE_ID,
			region: "side",
			title: "Tasks",
			scope: "conversation",
		},
		getSpec,
		invoke,
		subscribe(onChange) {
			subscribers.add(onChange);
			return () => {
				subscribers.delete(onChange);
			};
		},
	};

	registry.register(provider);

	host.logger.info("todo: registered");
}

export const extension: Extension = {
	manifest,
	activate,
};