@animalabs/connectome-host 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.env.example +20 -0
  2. package/.github/workflows/publish.yml +65 -0
  3. package/ARCHITECTURE.md +355 -0
  4. package/CHANGELOG.md +30 -0
  5. package/HEADLESS-FLEET-PLAN.md +330 -0
  6. package/README.md +189 -0
  7. package/UNIFIED-TREE-PLAN.md +242 -0
  8. package/bun.lock +288 -0
  9. package/docs/AGENT-MEMORY-GUIDE.md +214 -0
  10. package/docs/AGENT-ONBOARDING.md +541 -0
  11. package/docs/ATTENTION-AND-GATING.md +120 -0
  12. package/docs/DEPLOYMENTS.md +130 -0
  13. package/docs/DEV-ENVIRONMENT.md +215 -0
  14. package/docs/LIBRARY-PIPELINE.md +286 -0
  15. package/docs/LOCUS-ROUTING-DESIGN.md +115 -0
  16. package/docs/claudeai-evacuation.md +228 -0
  17. package/docs/debug-context-api.md +208 -0
  18. package/docs/webui-deployment.md +219 -0
  19. package/package.json +33 -0
  20. package/recipes/SETUP.md +308 -0
  21. package/recipes/TRIUMVIRATE-SETUP.md +467 -0
  22. package/recipes/claude-export-revive.json +19 -0
  23. package/recipes/clerk.json +157 -0
  24. package/recipes/knowledge-miner.json +174 -0
  25. package/recipes/knowledge-reviewer.json +76 -0
  26. package/recipes/mcpl-editor-test.json +38 -0
  27. package/recipes/prompts/transplant-addendum.md +17 -0
  28. package/recipes/triumvirate.json +63 -0
  29. package/recipes/webui-fleet-test.json +27 -0
  30. package/recipes/webui-test.json +16 -0
  31. package/recipes/zulip-miner.json +87 -0
  32. package/scripts/connectome-doctor +373 -0
  33. package/scripts/evacuator.ts +956 -0
  34. package/scripts/import-claudeai-export.ts +747 -0
  35. package/scripts/lib/line-reader.ts +53 -0
  36. package/scripts/test-historical-thinking.ts +148 -0
  37. package/scripts/warmup-session.ts +336 -0
  38. package/src/agent-name.ts +58 -0
  39. package/src/commands.ts +1074 -0
  40. package/src/headless.ts +528 -0
  41. package/src/index.ts +867 -0
  42. package/src/logging-adapter.ts +208 -0
  43. package/src/mcpl-config.ts +199 -0
  44. package/src/modules/activity-module.ts +270 -0
  45. package/src/modules/channel-mode-module.ts +260 -0
  46. package/src/modules/fleet-module.ts +1705 -0
  47. package/src/modules/fleet-types.ts +225 -0
  48. package/src/modules/lessons-module.ts +465 -0
  49. package/src/modules/mcpl-admin-module.ts +345 -0
  50. package/src/modules/retrieval-module.ts +327 -0
  51. package/src/modules/settings-module.ts +143 -0
  52. package/src/modules/subagent-module.ts +2074 -0
  53. package/src/modules/subscription-gc-module.ts +322 -0
  54. package/src/modules/time-module.ts +85 -0
  55. package/src/modules/tui-module.ts +76 -0
  56. package/src/modules/web-ui-curve-page.ts +249 -0
  57. package/src/modules/web-ui-module.ts +2595 -0
  58. package/src/recipe.ts +1003 -0
  59. package/src/session-manager.ts +278 -0
  60. package/src/state/agent-tree-reducer.ts +431 -0
  61. package/src/state/fleet-tree-aggregator.ts +195 -0
  62. package/src/strategies/frontdesk-strategy.ts +364 -0
  63. package/src/synesthete.ts +68 -0
  64. package/src/tui.ts +2200 -0
  65. package/src/types/bun-ffi.d.ts +6 -0
  66. package/src/web/protocol.ts +648 -0
  67. package/test/agent-name.test.ts +62 -0
  68. package/test/agent-tree-fleet-launch.test.ts +64 -0
  69. package/test/agent-tree-reducer-parity.test.ts +194 -0
  70. package/test/agent-tree-reducer.test.ts +443 -0
  71. package/test/agent-tree-rollup.test.ts +109 -0
  72. package/test/autobio-progress-snapshot.test.ts +40 -0
  73. package/test/claudeai-export-importer.test.ts +386 -0
  74. package/test/evacuator.test.ts +332 -0
  75. package/test/fleet-commands.test.ts +244 -0
  76. package/test/fleet-no-subfleets.test.ts +147 -0
  77. package/test/fleet-orchestration.test.ts +353 -0
  78. package/test/fleet-recipe.test.ts +124 -0
  79. package/test/fleet-route.test.ts +44 -0
  80. package/test/fleet-smoke.test.ts +479 -0
  81. package/test/fleet-subscribe-union-e2e.test.ts +123 -0
  82. package/test/fleet-subscribe-union.test.ts +85 -0
  83. package/test/fleet-tree-aggregator-e2e.test.ts +110 -0
  84. package/test/fleet-tree-aggregator.test.ts +215 -0
  85. package/test/frontdesk-strategy.test.ts +326 -0
  86. package/test/headless-describe.test.ts +182 -0
  87. package/test/headless-smoke.test.ts +190 -0
  88. package/test/logging-adapter.test.ts +87 -0
  89. package/test/mcpl-admin-module.test.ts +213 -0
  90. package/test/mcpl-agent-overlay.test.ts +126 -0
  91. package/test/mock-headless-child.ts +133 -0
  92. package/test/recipe-cache-ttl.test.ts +37 -0
  93. package/test/recipe-env.test.ts +157 -0
  94. package/test/recipe-path-resolution.test.ts +170 -0
  95. package/test/subagent-async-timeout.test.ts +381 -0
  96. package/test/subagent-fork-materialise.test.ts +241 -0
  97. package/test/subagent-peek-scoping.test.ts +180 -0
  98. package/test/subagent-peek-zombie.test.ts +148 -0
  99. package/test/subagent-reaper-in-flight.test.ts +229 -0
  100. package/test/subscription-gc-module.test.ts +136 -0
  101. package/test/warmup-handoff.test.ts +150 -0
  102. package/test/web-ui-bind.test.ts +51 -0
  103. package/test/web-ui-module.test.ts +246 -0
  104. package/test/web-ui-protocol.test.ts +0 -0
  105. package/tsconfig.json +14 -0
  106. package/web/bun.lock +357 -0
  107. package/web/index.html +12 -0
  108. package/web/package.json +24 -0
  109. package/web/src/App.tsx +1931 -0
  110. package/web/src/Context.tsx +158 -0
  111. package/web/src/ContextDocument.tsx +150 -0
  112. package/web/src/Files.tsx +271 -0
  113. package/web/src/Lessons.tsx +164 -0
  114. package/web/src/Mcpl.tsx +310 -0
  115. package/web/src/Stream.tsx +119 -0
  116. package/web/src/TreeSidebar.tsx +283 -0
  117. package/web/src/Usage.tsx +182 -0
  118. package/web/src/main.tsx +7 -0
  119. package/web/src/styles.css +26 -0
  120. package/web/src/tree.ts +268 -0
  121. package/web/src/wire.ts +120 -0
  122. package/web/tsconfig.json +21 -0
  123. package/web/vite.config.ts +32 -0
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Client-side tree state — re-uses the AgentTreeReducer from conhost via the
3
+ * @conhost/state path alias, fed by `trace` and `child-event` messages over
4
+ * WS. Same reducer, four call sites now (parent-local, parent-per-child,
5
+ * child-side describe handler, the browser).
6
+ *
7
+ * The browser presents a *single unified tree* mirroring the TUI's fleet
8
+ * view: one synthetic parent-process root, with local framework agents and
9
+ * subagents inline, plus each fleet child as a folder-like node containing
10
+ * its own subtree. The per-scope reducers under the hood are an implementation
11
+ * detail — `buildUiTree()` weaves their roots into one hierarchy.
12
+ */
13
+
14
+ import { createSignal, type Accessor } from 'solid-js';
15
+ import { AgentTreeReducer, type AgentNode, type AgentTreeSnapshot } from '@conhost/state/agent-tree-reducer';
16
+ import type { WebUiServerMessage } from '@conhost/web/protocol';
17
+
18
+ export type ScopeId = 'local' | string;
19
+
20
+ /** Stream subscription strategy for a UI node. Decides what events are
21
+ * routed into the stream pane when the node is selected. */
22
+ export type StreamSource =
23
+ | { kind: 'none' } // parent process / framework agent: main pane already covers it
24
+ | { kind: 'peek'; scope: string } // local subagent: open subscribe-peek
25
+ | { kind: 'child-event-all'; childName: string } // fleet-child folder: every child-event for this child
26
+ | { kind: 'child-event-agent'; childName: string; agentName: string }; // agent inside a fleet child: filter child-events by agentName
27
+
28
+ export type UiNodeKind = 'process' | 'framework' | 'subagent' | 'fleet-child';
29
+
30
+ export interface UiNode {
31
+ /** Stable id used as expand/collapse key and React-style key. */
32
+ id: string;
33
+ kind: UiNodeKind;
34
+ label: string;
35
+ /** Underlying reducer node, when this UI node represents an agent. */
36
+ agent?: AgentNode;
37
+ /** For 'fleet-child', the child-process name (used by stop/restart). */
38
+ fleetChildName?: string;
39
+ streamSource: StreamSource;
40
+ children: UiNode[];
41
+ }
42
+
43
+ export interface TreeStore {
44
+ /** Reactive accessor — fires whenever any reducer's state changes. */
45
+ build: Accessor<UiNode[]>;
46
+ /** Apply a server message; idempotent for non-tree messages. */
47
+ ingest(msg: WebUiServerMessage): void;
48
+ }
49
+
50
+ interface ScopeState {
51
+ reducer: AgentTreeReducer;
52
+ /** asOfTs of the last applied snapshot; events older than this are dropped. */
53
+ lastSnapshotTs: number;
54
+ /** True once we've ever applied a snapshot; gates stale-event filtering. */
55
+ hasSnapshot: boolean;
56
+ }
57
+
58
+ export function createTreeStore(): TreeStore {
59
+ const states = new Map<ScopeId, ScopeState>();
60
+ const [version, setVersion] = createSignal(0);
61
+ let parentLabel = 'parent';
62
+
63
+ const ensureScope = (scope: ScopeId): ScopeState => {
64
+ let s = states.get(scope);
65
+ if (!s) {
66
+ s = { reducer: new AgentTreeReducer(), lastSnapshotTs: 0, hasSnapshot: false };
67
+ states.set(scope, s);
68
+ }
69
+ return s;
70
+ };
71
+
72
+ const applySnapshot = (scope: ScopeId, snap: AgentTreeSnapshot): void => {
73
+ const s = ensureScope(scope);
74
+ s.reducer.applySnapshot(snap);
75
+ s.lastSnapshotTs = snap.asOfTs;
76
+ s.hasSnapshot = true;
77
+ bump();
78
+ };
79
+
80
+ const applyEvent = (scope: ScopeId, event: { type: string; [k: string]: unknown }): void => {
81
+ const s = ensureScope(scope);
82
+ // Drop stale events that pre-date the most recent snapshot — they're
83
+ // already reflected in its state. Mirrors FleetTreeAggregator dedup logic.
84
+ const ts = typeof event.timestamp === 'number' ? event.timestamp
85
+ : typeof (event as { ts?: unknown }).ts === 'number' ? ((event as unknown) as { ts: number }).ts
86
+ : undefined;
87
+ if (s.hasSnapshot && typeof ts === 'number' && ts < s.lastSnapshotTs) return;
88
+ s.reducer.applyEvent(event);
89
+ bump();
90
+ };
91
+
92
+ const bump = (): void => { setVersion((v) => v + 1); };
93
+
94
+ const build: Accessor<UiNode[]> = () => {
95
+ void version();
96
+
97
+ const childScopes = [...states.keys()]
98
+ .filter(s => s !== 'local')
99
+ .sort((a, b) => a.localeCompare(b));
100
+
101
+ const localChildren: UiNode[] = [];
102
+ const localState = states.get('local');
103
+ if (localState) {
104
+ const allLocal = localState.reducer.getNodes();
105
+ const byName = indexByName(allLocal);
106
+ for (const root of localState.reducer.getRoots()) {
107
+ localChildren.push(buildAgentSubtree(root, byName, undefined));
108
+ }
109
+ }
110
+
111
+ for (const scope of childScopes) {
112
+ const s = states.get(scope)!;
113
+ const childNodes = s.reducer.getNodes();
114
+ const byName = indexByName(childNodes);
115
+ const roots = s.reducer.getRoots();
116
+ const subtree = roots.map(r => buildAgentSubtree(r, byName, scope));
117
+ localChildren.push({
118
+ id: `fleet:${scope}`,
119
+ kind: 'fleet-child',
120
+ label: scope,
121
+ fleetChildName: scope,
122
+ streamSource: { kind: 'child-event-all', childName: scope },
123
+ children: subtree,
124
+ });
125
+ }
126
+
127
+ const parent: UiNode = {
128
+ id: 'process:local',
129
+ kind: 'process',
130
+ label: parentLabel,
131
+ streamSource: { kind: 'none' },
132
+ children: localChildren,
133
+ };
134
+ return [parent];
135
+ };
136
+
137
+ const ingest = (msg: WebUiServerMessage): void => {
138
+ switch (msg.type) {
139
+ case 'welcome': {
140
+ states.clear();
141
+ parentLabel = msg.recipe.name || 'parent';
142
+ applySnapshot('local', {
143
+ asOfTs: msg.localTree.asOfTs,
144
+ nodes: msg.localTree.nodes as unknown as AgentNode[],
145
+ callIdIndex: msg.localTree.callIdIndex,
146
+ });
147
+ for (const child of msg.childTrees) {
148
+ applySnapshot(child.name, {
149
+ asOfTs: child.asOfTs,
150
+ nodes: child.nodes as unknown as AgentNode[],
151
+ callIdIndex: child.callIdIndex,
152
+ });
153
+ }
154
+ return;
155
+ }
156
+ case 'trace':
157
+ applyEvent('local', msg.event);
158
+ return;
159
+ case 'child-event': {
160
+ const e = msg.event;
161
+ if (e.type === 'snapshot') {
162
+ const tree = (e as unknown as { tree?: { nodes?: unknown[]; callIdIndex?: Record<string, string> }; asOfTs?: number }).tree;
163
+ const asOfTs = (e as unknown as { asOfTs?: number }).asOfTs ?? Date.now();
164
+ applySnapshot(msg.childName, {
165
+ asOfTs,
166
+ nodes: (tree?.nodes ?? []) as unknown as AgentNode[],
167
+ callIdIndex: tree?.callIdIndex ?? {},
168
+ });
169
+ return;
170
+ }
171
+ applyEvent(msg.childName, e);
172
+ return;
173
+ }
174
+ default:
175
+ return;
176
+ }
177
+ };
178
+
179
+ return { build, ingest };
180
+ }
181
+
182
+ function indexByName(nodes: AgentNode[]): Map<string, AgentNode> {
183
+ const m = new Map<string, AgentNode>();
184
+ for (const n of nodes) m.set(n.name, n);
185
+ return m;
186
+ }
187
+
188
+ function buildAgentSubtree(
189
+ node: AgentNode,
190
+ byName: Map<string, AgentNode>,
191
+ fleetChildName: string | undefined,
192
+ ): UiNode {
193
+ const children: UiNode[] = [];
194
+ for (const candidate of byName.values()) {
195
+ if (candidate.parent === node.name) {
196
+ children.push(buildAgentSubtree(candidate, byName, fleetChildName));
197
+ }
198
+ }
199
+ const kind: UiNodeKind = node.kind === 'subagent' ? 'subagent' : 'framework';
200
+ return {
201
+ id: fleetChildName ? `fleet:${fleetChildName}:${node.name}` : `local:${node.name}`,
202
+ kind,
203
+ label: node.name,
204
+ agent: node,
205
+ streamSource: pickStreamSource(node, fleetChildName),
206
+ children,
207
+ };
208
+ }
209
+
210
+ function pickStreamSource(node: AgentNode, fleetChildName: string | undefined): StreamSource {
211
+ if (fleetChildName) {
212
+ return { kind: 'child-event-agent', childName: fleetChildName, agentName: node.name };
213
+ }
214
+ if (node.kind === 'subagent') {
215
+ return { kind: 'peek', scope: node.name };
216
+ }
217
+ // Local framework agent: the main pane already shows its activity.
218
+ return { kind: 'none' };
219
+ }
220
+
221
+ /** Walk a UiNode tree depth-first, honoring an expanded-id set, and produce a
222
+ * flat list with depth info for rendering. The tree is small enough that
223
+ * flattening per render is fine. */
224
+ export interface FlatUiNode {
225
+ node: UiNode;
226
+ depth: number;
227
+ }
228
+
229
+ export function flattenUiTree(roots: UiNode[], expanded: Set<string>): FlatUiNode[] {
230
+ const out: FlatUiNode[] = [];
231
+ const visit = (node: UiNode, depth: number): void => {
232
+ out.push({ node, depth });
233
+ if (expanded.has(node.id)) {
234
+ for (const child of node.children) visit(child, depth + 1);
235
+ }
236
+ };
237
+ for (const r of roots) visit(r, 0);
238
+ return out;
239
+ }
240
+
241
+ export interface AggregateTokens {
242
+ /** Sum of current context window sizes across descendants. Useful as a
243
+ * "total active memory burden" indicator for aggregate nodes; for a single
244
+ * agent it equals that agent's current input. */
245
+ input: number;
246
+ /** Cumulative output across descendants. */
247
+ output: number;
248
+ cacheRead: number;
249
+ cacheWrite: number;
250
+ }
251
+
252
+ /** Walk a UiNode subtree and aggregate token counts. Cumulative fields
253
+ * (output / cache) sum across descendants; input also sums since each
254
+ * agent's input is its own current ctx and they don't overlap. */
255
+ export function aggregateTokens(node: UiNode): AggregateTokens {
256
+ const agg: AggregateTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
257
+ const visit = (n: UiNode): void => {
258
+ if (n.agent) {
259
+ agg.input += n.agent.tokens.input;
260
+ agg.output += n.agent.tokens.output;
261
+ agg.cacheRead += n.agent.tokens.cacheRead;
262
+ agg.cacheWrite += n.agent.tokens.cacheWrite;
263
+ }
264
+ for (const c of n.children) visit(c);
265
+ };
266
+ visit(node);
267
+ return agg;
268
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * WS client with exponential-backoff reconnect.
3
+ *
4
+ * Designed for one connection per page; the SPA holds a single instance.
5
+ * Subscriptions are signal-based: callers watch `messages` / `status` and
6
+ * react to changes.
7
+ */
8
+
9
+ import { createSignal, type Accessor } from 'solid-js';
10
+ import type {
11
+ WebUiClientMessage,
12
+ WebUiServerMessage,
13
+ } from '@conhost/web/protocol';
14
+
15
+ export type ConnectionStatus = 'connecting' | 'open' | 'reconnecting' | 'closed';
16
+
17
+ export interface WireClient {
18
+ status: Accessor<ConnectionStatus>;
19
+ /** Last received message, or null. Useful for reactive folds. */
20
+ lastMessage: Accessor<WebUiServerMessage | null>;
21
+ /** Subscribe to all incoming messages. Returns an unsubscribe fn. */
22
+ onMessage(handler: (msg: WebUiServerMessage) => void): () => void;
23
+ /** Send a message. No-ops with a console warning if the socket is closed. */
24
+ send(msg: WebUiClientMessage): void;
25
+ /** Manually close the connection (also cancels reconnects). */
26
+ close(): void;
27
+ }
28
+
29
+ export interface WireOptions {
30
+ /** WS URL. Default: derives from window.location with /ws path. */
31
+ url?: string;
32
+ /** Initial reconnect delay in ms. Default: 500. */
33
+ initialDelayMs?: number;
34
+ /** Max reconnect delay in ms. Default: 8000. */
35
+ maxDelayMs?: number;
36
+ }
37
+
38
+ export function createWireClient(opts: WireOptions = {}): WireClient {
39
+ const url = opts.url ?? defaultWsUrl();
40
+ const initialDelay = opts.initialDelayMs ?? 500;
41
+ const maxDelay = opts.maxDelayMs ?? 8000;
42
+
43
+ const [status, setStatus] = createSignal<ConnectionStatus>('connecting');
44
+ const [lastMessage, setLastMessage] = createSignal<WebUiServerMessage | null>(null);
45
+ const handlers = new Set<(m: WebUiServerMessage) => void>();
46
+
47
+ let socket: WebSocket | null = null;
48
+ let stopped = false;
49
+ let delay = initialDelay;
50
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
51
+
52
+ function connect(): void {
53
+ if (stopped) return;
54
+ setStatus(socket ? 'reconnecting' : 'connecting');
55
+ socket = new WebSocket(url);
56
+
57
+ socket.addEventListener('open', () => {
58
+ delay = initialDelay;
59
+ setStatus('open');
60
+ });
61
+
62
+ socket.addEventListener('message', (event) => {
63
+ let parsed: WebUiServerMessage;
64
+ try {
65
+ parsed = JSON.parse(event.data) as WebUiServerMessage;
66
+ } catch {
67
+ console.warn('[wire] failed to parse message', event.data);
68
+ return;
69
+ }
70
+ setLastMessage(parsed);
71
+ for (const h of handlers) {
72
+ try { h(parsed); } catch (err) { console.error('[wire] handler threw', err); }
73
+ }
74
+ });
75
+
76
+ socket.addEventListener('close', () => {
77
+ socket = null;
78
+ if (stopped) {
79
+ setStatus('closed');
80
+ return;
81
+ }
82
+ setStatus('reconnecting');
83
+ reconnectTimer = setTimeout(connect, delay);
84
+ delay = Math.min(delay * 2, maxDelay);
85
+ });
86
+
87
+ socket.addEventListener('error', () => {
88
+ // Errors trigger a close immediately after; the close handler does the work.
89
+ });
90
+ }
91
+
92
+ connect();
93
+
94
+ return {
95
+ status,
96
+ lastMessage,
97
+ onMessage(handler) {
98
+ handlers.add(handler);
99
+ return () => { handlers.delete(handler); };
100
+ },
101
+ send(msg) {
102
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
103
+ console.warn('[wire] dropping message; socket not open', msg.type);
104
+ return;
105
+ }
106
+ socket.send(JSON.stringify(msg));
107
+ },
108
+ close() {
109
+ stopped = true;
110
+ if (reconnectTimer) clearTimeout(reconnectTimer);
111
+ socket?.close();
112
+ setStatus('closed');
113
+ },
114
+ };
115
+ }
116
+
117
+ function defaultWsUrl(): string {
118
+ const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
119
+ return `${proto}//${window.location.host}/ws`;
120
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "jsx": "preserve",
10
+ "jsxImportSource": "solid-js",
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "allowImportingTsExtensions": true,
14
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
15
+ "paths": {
16
+ "@conhost/state/*": ["../src/state/*"],
17
+ "@conhost/web/*": ["../src/web/*"]
18
+ }
19
+ },
20
+ "include": ["src", "vite.config.ts"]
21
+ }
@@ -0,0 +1,32 @@
1
+ import { defineConfig } from 'vite';
2
+ import solid from 'vite-plugin-solid';
3
+ import tailwind from '@tailwindcss/vite';
4
+ import { fileURLToPath, URL } from 'node:url';
5
+
6
+ export default defineConfig({
7
+ plugins: [solid(), tailwind()],
8
+ resolve: {
9
+ alias: {
10
+ // Share state code (AgentTreeReducer, etc.) with conhost server-side.
11
+ // The reducer is pure TS with no Node deps; safe to import from the SPA.
12
+ '@conhost/state': fileURLToPath(new URL('../src/state', import.meta.url)),
13
+ '@conhost/web': fileURLToPath(new URL('../src/web', import.meta.url)),
14
+ },
15
+ },
16
+ build: {
17
+ // Match WebUiModule's default staticDir (../dist/web).
18
+ outDir: '../dist/web',
19
+ emptyOutDir: true,
20
+ },
21
+ server: {
22
+ // Dev server proxies WS to the running conhost so `bun run dev` in web/
23
+ // talks to a real backend. Port matches WebUiModule default.
24
+ proxy: {
25
+ '/ws': {
26
+ target: 'ws://127.0.0.1:7340',
27
+ ws: true,
28
+ changeOrigin: true,
29
+ },
30
+ },
31
+ },
32
+ });