agents-dojo 0.1.6 → 0.1.7

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 (50) hide show
  1. package/monitor/.env.development +1 -0
  2. package/monitor/.env.production +1 -0
  3. package/monitor/ANIMATION_REQUIREMENTS.md +118 -0
  4. package/monitor/index.html +12 -0
  5. package/monitor/package-lock.json +2160 -0
  6. package/monitor/package.json +25 -0
  7. package/monitor/public/bg.png +0 -0
  8. package/monitor/public/bg_clean.png +0 -0
  9. package/monitor/public/positions.json +215 -0
  10. package/monitor/public/sprites/agent_default.png +0 -0
  11. package/monitor/public/sprites/cushion_0.png +0 -0
  12. package/monitor/public/sprites/cushion_1.png +0 -0
  13. package/monitor/public/sprites/cushion_10.png +0 -0
  14. package/monitor/public/sprites/cushion_2.png +0 -0
  15. package/monitor/public/sprites/cushion_3.png +0 -0
  16. package/monitor/public/sprites/cushion_4.png +0 -0
  17. package/monitor/public/sprites/cushion_5.png +0 -0
  18. package/monitor/public/sprites/cushion_6.png +0 -0
  19. package/monitor/public/sprites/cushion_7.png +0 -0
  20. package/monitor/public/sprites/cushion_8.png +0 -0
  21. package/monitor/public/sprites/cushion_9.png +0 -0
  22. package/monitor/public/sprites/master.png +0 -0
  23. package/monitor/public/sprites/stake_0.png +0 -0
  24. package/monitor/public/sprites/stake_1.png +0 -0
  25. package/monitor/public/sprites/stake_10.png +0 -0
  26. package/monitor/public/sprites/stake_2.png +0 -0
  27. package/monitor/public/sprites/stake_3.png +0 -0
  28. package/monitor/public/sprites/stake_4.png +0 -0
  29. package/monitor/public/sprites/stake_5.png +0 -0
  30. package/monitor/public/sprites/stake_6.png +0 -0
  31. package/monitor/public/sprites/stake_7.png +0 -0
  32. package/monitor/public/sprites/stake_8.png +0 -0
  33. package/monitor/public/sprites/stake_9.png +0 -0
  34. package/monitor/scripts/record-gif.py +53 -0
  35. package/monitor/src/App.tsx +22 -0
  36. package/monitor/src/components/AgentMenu.tsx +67 -0
  37. package/monitor/src/components/ChatPanel.tsx +214 -0
  38. package/monitor/src/components/LogPage.tsx +173 -0
  39. package/monitor/src/components/Stage.tsx +39 -0
  40. package/monitor/src/components/StatusBar.tsx +50 -0
  41. package/monitor/src/lib/dojo-app.ts +799 -0
  42. package/monitor/src/lib/interactables.ts +162 -0
  43. package/monitor/src/lib/store.ts +352 -0
  44. package/monitor/src/lib/types.ts +72 -0
  45. package/monitor/src/lib/ws-client.ts +66 -0
  46. package/monitor/src/main.tsx +9 -0
  47. package/monitor/src/vite-env.d.ts +1 -0
  48. package/monitor/tsconfig.json +14 -0
  49. package/monitor/vite.config.ts +13 -0
  50. package/package.json +2 -1
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Interactable framework — unified system for objects that agents interact with.
3
+ *
4
+ * Each interactable defines:
5
+ * - Its position and collision shape
6
+ * - Where the agent should stand/sit to interact
7
+ * - What animation the agent plays during interaction
8
+ * - What animation the object plays when interacted with
9
+ *
10
+ * To add a new interactable type:
11
+ * 1. Add an entry to INTERACTABLE_DEFS below
12
+ * 2. Create a sprite component using the InteractableSprite helper
13
+ * 3. Register it in Stage.tsx
14
+ */
15
+
16
+ import positionsData from '../../public/positions.json';
17
+
18
+ // ── Types ──────────────────────────────────────────────────
19
+
20
+ export type InteractableType = 'stake' | 'cushion';
21
+
22
+ export interface InteractableDef {
23
+ type: InteractableType;
24
+
25
+ /** Anchor position in design coords (where the object sits) */
26
+ anchorX: number;
27
+ anchorY: number;
28
+
29
+ /** Where the agent should move to interact (feet position) */
30
+ agentX: number;
31
+ agentY: number;
32
+
33
+ /** Which direction the agent faces during interaction (true = face left) */
34
+ agentFaceLeft: boolean;
35
+
36
+ /** Agent animation during interaction */
37
+ agentAnim: 'sit' | 'attack' | 'idle';
38
+
39
+ /** Collision radius for wander avoidance */
40
+ collisionRadius: number;
41
+
42
+ /** Sprite file path */
43
+ spriteFile: string;
44
+
45
+ /** Original pixel dimensions (for scaling to design space) */
46
+ origW: number;
47
+ origH: number;
48
+
49
+ /** Rendering: anchor point of the sprite */
50
+ spriteAnchorX: number;
51
+ spriteAnchorY: number;
52
+
53
+ /** Rendering: y position for the sprite */
54
+ spriteY: number;
55
+
56
+ /** Index for matching to agent assignments */
57
+ index: number;
58
+ }
59
+
60
+ // ── Constants ──────────────────────────────────────────────
61
+
62
+ const DS = 960 / 2400; // design scale factor
63
+
64
+ // Stake: 104px wide in original → 41.6px design → half = ~21px
65
+ const STAKE_HALF_W = 21;
66
+ const STAKE_AGENT_GAP = 4; // gap between agent and stake edge
67
+ const STAKE_AGENT_OFFSET_X = STAKE_HALF_W + STAKE_AGENT_GAP; // 25px
68
+
69
+ // Cushion: agent sits slightly above cushion center so body overlaps the cushion
70
+ const CUSHION_SIT_OFFSET_Y = 2;
71
+
72
+ // ── Build interactable list from positions data ────────────
73
+
74
+ export const INTERACTABLES: InteractableDef[] = [];
75
+
76
+ // Stakes
77
+ for (let i = 0; i < positionsData.stakes.length; i++) {
78
+ const s = positionsData.stakes[i];
79
+ INTERACTABLES.push({
80
+ type: 'stake',
81
+ anchorX: s.x,
82
+ anchorY: (s.y_top + s.y_bot) / 2,
83
+ // Agent stands beside stake, feet at stake bottom y
84
+ agentX: s.x + STAKE_AGENT_OFFSET_X,
85
+ agentY: s.y_bot,
86
+ agentFaceLeft: true,
87
+ agentAnim: 'attack',
88
+ collisionRadius: 20,
89
+ spriteFile: `/sprites/stake_${i}.png`,
90
+ origW: s.w,
91
+ origH: s.h,
92
+ spriteAnchorX: 0.5,
93
+ spriteAnchorY: 0,
94
+ spriteY: s.y_top,
95
+ index: i,
96
+ });
97
+ }
98
+
99
+ // Cushions (skip the 11th lone cushion if > 10 agents)
100
+ const cushionCount = Math.min(positionsData.cushions.length, 10);
101
+ for (let i = 0; i < cushionCount; i++) {
102
+ const c = positionsData.cushions[i];
103
+ INTERACTABLES.push({
104
+ type: 'cushion',
105
+ anchorX: c.x,
106
+ anchorY: c.y,
107
+ // Agent sits at cushion center x, shifted down for overlap
108
+ agentX: c.x,
109
+ agentY: c.y + CUSHION_SIT_OFFSET_Y,
110
+ agentFaceLeft: false,
111
+ agentAnim: 'sit',
112
+ collisionRadius: 18,
113
+ spriteFile: `/sprites/cushion_${i}.png`,
114
+ origW: c.w,
115
+ origH: c.h,
116
+ spriteAnchorX: 0.5,
117
+ spriteAnchorY: 0.5,
118
+ spriteY: c.y,
119
+ index: i,
120
+ });
121
+ }
122
+
123
+ // ── Helpers for agent assignment ───────────────────────────
124
+
125
+ /** Get all interactables of a given type, sorted by index */
126
+ export function getByType(type: InteractableType): InteractableDef[] {
127
+ return INTERACTABLES.filter((i) => i.type === type).sort((a, b) => a.index - b.index);
128
+ }
129
+
130
+ /** Get obstacles for wander collision avoidance */
131
+ export function getObstacles(): Array<{ x: number; y: number; r: number }> {
132
+ return INTERACTABLES.map((i) => ({ x: i.anchorX, y: i.anchorY, r: i.collisionRadius }));
133
+ }
134
+
135
+ /** Pair cushions and stakes 1:1 by proximity (returns paired arrays) */
136
+ export function getPairedPositions(): Array<{
137
+ cushion: InteractableDef;
138
+ stake: InteractableDef;
139
+ }> {
140
+ const cushions = getByType('cushion');
141
+ const stakes = getByType('stake');
142
+ const usedStakes = new Set<number>();
143
+ const pairs: Array<{ cushion: InteractableDef; stake: InteractableDef }> = [];
144
+
145
+ for (const c of cushions) {
146
+ let bestIdx = -1;
147
+ let bestDist = Infinity;
148
+ for (let j = 0; j < stakes.length; j++) {
149
+ if (usedStakes.has(j)) continue;
150
+ const dist = Math.abs(stakes[j].anchorX - c.anchorX);
151
+ if (dist < bestDist) {
152
+ bestDist = dist;
153
+ bestIdx = j;
154
+ }
155
+ }
156
+ if (bestIdx >= 0) {
157
+ usedStakes.add(bestIdx);
158
+ pairs.push({ cushion: c, stake: stakes[bestIdx] });
159
+ }
160
+ }
161
+ return pairs;
162
+ }
@@ -0,0 +1,352 @@
1
+ import { create } from 'zustand';
2
+ import type { Agent, AgentState, AgentAnim, ChatSession, MessageLink, MonitorEvent, UserChat, ChatMessage } from './types.js';
3
+ import { getPairedPositions } from './interactables.js';
4
+
5
+ const PAIRED = getPairedPositions();
6
+
7
+ // ── localStorage persistence (position + state) ────────────
8
+
9
+ const STORAGE_KEY = 'dojo-monitor-agents';
10
+
11
+ interface SavedAgent {
12
+ position: { x: number; y: number };
13
+ state: AgentState;
14
+ taskFrom?: string;
15
+ }
16
+
17
+ function loadSavedAgents(): Map<string, SavedAgent> {
18
+ try {
19
+ const raw = localStorage.getItem(STORAGE_KEY);
20
+ if (!raw) return new Map();
21
+ const obj = JSON.parse(raw) as Record<string, SavedAgent>;
22
+ return new Map(Object.entries(obj));
23
+ } catch { return new Map(); }
24
+ }
25
+
26
+ let saveTimer: ReturnType<typeof setTimeout> | null = null;
27
+ function scheduleSave(agents: Map<string, Agent>) {
28
+ if (saveTimer) return; // already scheduled
29
+ saveTimer = setTimeout(() => {
30
+ saveTimer = null;
31
+ try {
32
+ const obj: Record<string, SavedAgent> = {};
33
+ for (const [id, a] of agents) {
34
+ obj[id] = { position: a.position, state: a.state, taskFrom: a.taskFrom };
35
+ }
36
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(obj));
37
+ } catch { /* ignore quota errors */ }
38
+ }, 500);
39
+ }
40
+
41
+ interface MonitorStore {
42
+ agents: Map<string, Agent>;
43
+ links: Map<string, MessageLink>;
44
+ chatSessions: Map<string, ChatSession>;
45
+ taskToAgent: Map<string, string>;
46
+ connected: boolean;
47
+ agentIndex: number;
48
+
49
+ // User chat (monitor GUI → agent)
50
+ userChat: UserChat | null;
51
+ // Master bubble message (client's message shown on master sprite)
52
+ masterMessage: string | null;
53
+ // Context menu
54
+ menuAgentId: string | null;
55
+ menuPosition: { x: number; y: number } | null;
56
+
57
+ applyEvent: (event: MonitorEvent) => void;
58
+ setConnected: (b: boolean) => void;
59
+ setPosition: (id: string, x: number, y: number) => void;
60
+ setAnim: (id: string, anim: AgentAnim) => void;
61
+
62
+ // Chat session API
63
+ createChatSession: (session: ChatSession) => void;
64
+ endChatSession: (sessionId: string) => void;
65
+ getAgentSession: (agentId: string) => ChatSession | undefined;
66
+
67
+ // User chat actions
68
+ openChat: (agentId: string) => void;
69
+ closeChat: () => void;
70
+ addChatMessage: (msg: ChatMessage) => void;
71
+ setChatLoading: (loading: boolean) => void;
72
+
73
+ // Context menu actions
74
+ openMenu: (agentId: string, x: number, y: number) => void;
75
+ closeMenu: () => void;
76
+ }
77
+
78
+ /**
79
+ * Compute where each member stands around the meetPoint.
80
+ * Members form a circle/line around the center point.
81
+ */
82
+ export function getChatPosition(meetPoint: { x: number; y: number }, memberIndex: number, totalMembers: number): { x: number; y: number } {
83
+ const SPACING = 30;
84
+ if (totalMembers <= 2) {
85
+ // Two people: side by side
86
+ const offset = memberIndex === 0 ? -SPACING / 2 : SPACING / 2;
87
+ return { x: meetPoint.x + offset, y: meetPoint.y };
88
+ }
89
+ // N people: arrange in a circle
90
+ const angle = (2 * Math.PI * memberIndex) / totalMembers - Math.PI / 2;
91
+ const radius = SPACING;
92
+ return {
93
+ x: meetPoint.x + Math.cos(angle) * radius,
94
+ y: meetPoint.y + Math.sin(angle) * radius * 0.6, // squash y for pseudo-isometric
95
+ };
96
+ }
97
+
98
+ export const useMonitorStore = create<MonitorStore>((set, get) => {
99
+ if (typeof window !== 'undefined') {
100
+ (window as any).__DOJO_STORE__ = { getState: () => get() };
101
+ }
102
+ return ({
103
+ agents: new Map(),
104
+ links: new Map(),
105
+ chatSessions: new Map(),
106
+ taskToAgent: new Map(),
107
+ connected: false,
108
+ agentIndex: 0,
109
+ userChat: null,
110
+ masterMessage: null,
111
+ menuAgentId: null,
112
+ menuPosition: null,
113
+
114
+ setConnected: (b) => set({ connected: b }),
115
+
116
+ setPosition: (id, x, y) => {
117
+ const a = get().agents.get(id);
118
+ // Skip if position hasn't actually changed (avoids unnecessary re-renders)
119
+ if (!a || (Math.abs(a.position.x - x) < 0.5 && Math.abs(a.position.y - y) < 0.5)) return;
120
+ const agents = new Map(get().agents);
121
+ agents.set(id, { ...a, position: { x, y } });
122
+ set({ agents });
123
+ scheduleSave(agents);
124
+ },
125
+
126
+ setAnim: (id, anim) => {
127
+ const agents = new Map(get().agents);
128
+ const a = agents.get(id);
129
+ if (a && a.currentAnim !== anim) {
130
+ agents.set(id, { ...a, currentAnim: anim });
131
+ set({ agents });
132
+ }
133
+ },
134
+
135
+ createChatSession: (session) => {
136
+ const sessions = new Map(get().chatSessions);
137
+ const agents = new Map(get().agents);
138
+ sessions.set(session.id, session);
139
+ // Tag each member with the session ID
140
+ for (const mid of session.memberIds) {
141
+ const a = agents.get(mid);
142
+ if (a) agents.set(mid, { ...a, chatSessionId: session.id });
143
+ }
144
+ set({ chatSessions: sessions, agents });
145
+ },
146
+
147
+ endChatSession: (sessionId) => {
148
+ const sessions = new Map(get().chatSessions);
149
+ const agents = new Map(get().agents);
150
+ const session = sessions.get(sessionId);
151
+ if (session) {
152
+ for (const mid of session.memberIds) {
153
+ const a = agents.get(mid);
154
+ if (a) agents.set(mid, { ...a, chatSessionId: undefined });
155
+ }
156
+ sessions.delete(sessionId);
157
+ }
158
+ set({ chatSessions: sessions, agents });
159
+ },
160
+
161
+ getAgentSession: (agentId) => {
162
+ const a = get().agents.get(agentId);
163
+ if (!a?.chatSessionId) return undefined;
164
+ return get().chatSessions.get(a.chatSessionId);
165
+ },
166
+
167
+ // User chat actions
168
+ openChat: (agentId) => set({ userChat: { agentId, messages: [], loading: false }, menuAgentId: null, menuPosition: null }),
169
+ closeChat: () => set({ userChat: null }),
170
+ addChatMessage: (msg) => {
171
+ const chat = get().userChat;
172
+ if (!chat) return;
173
+ set({ userChat: { ...chat, messages: [...chat.messages, msg] } });
174
+ },
175
+ setChatLoading: (loading) => {
176
+ const chat = get().userChat;
177
+ if (!chat) return;
178
+ set({ userChat: { ...chat, loading } });
179
+ },
180
+
181
+ // Context menu actions
182
+ openMenu: (agentId, x, y) => set({ menuAgentId: agentId, menuPosition: { x, y } }),
183
+ closeMenu: () => set({ menuAgentId: null, menuPosition: null }),
184
+
185
+ applyEvent: (event) => {
186
+ const agents = new Map(get().agents);
187
+ const links = new Map(get().links);
188
+ const taskToAgent = new Map(get().taskToAgent);
189
+
190
+ switch (event.type) {
191
+ case 'agent_loaded': {
192
+ if (!agents.has(event.agentId)) {
193
+ const idx = get().agentIndex;
194
+ const pair = PAIRED[idx % PAIRED.length];
195
+ const home = { x: pair.cushion.agentX, y: pair.cushion.agentY };
196
+ const saved = loadSavedAgents().get(event.agentId);
197
+ agents.set(event.agentId, {
198
+ id: event.agentId,
199
+ position: saved?.position ?? { ...home },
200
+ homePosition: { ...home },
201
+ stakePosition: { x: pair.stake.anchorX, y: pair.stake.agentY },
202
+ sprite: 'default',
203
+ state: saved?.state ?? 'idle',
204
+ currentAnim: 'idle',
205
+ taskFrom: saved?.taskFrom,
206
+ });
207
+ set({ agentIndex: idx + 1 });
208
+ }
209
+ break;
210
+ }
211
+ case 'task_created': {
212
+ taskToAgent.set(event.taskId, event.to);
213
+
214
+ // Detect agent-to-agent call: if another agent is currently working/tool_call,
215
+ // it likely called this agent via the call-agent skill.
216
+ let callerAgentId: string | undefined;
217
+ for (const [id, a] of agents) {
218
+ if (id !== event.to && (a.state === 'working' || a.state === 'tool_call')) {
219
+ callerAgentId = id;
220
+ break;
221
+ }
222
+ }
223
+
224
+ const fromId = callerAgentId ?? event.from;
225
+ const link: MessageLink = {
226
+ id: event.taskId,
227
+ from: fromId,
228
+ to: event.to,
229
+ preview: event.preview, startTime: Date.now(), taskId: event.taskId,
230
+ };
231
+ links.set(link.id, link);
232
+ const to = agents.get(event.to);
233
+ // Client's message is shown on master, not on the agent
234
+ if (!callerAgentId) {
235
+ set({ masterMessage: event.preview });
236
+ }
237
+ if (to) agents.set(to.id, { ...to, state: 'submitted', taskFrom: fromId });
238
+ if (callerAgentId) {
239
+ // Agent-to-agent call — create a chat session
240
+ const sessionId = `peer-${event.taskId}`;
241
+ const meetPoint = {
242
+ x: ((agents.get(callerAgentId)?.position.x ?? 480) + (to?.position.x ?? 480)) / 2,
243
+ y: Math.max(agents.get(callerAgentId)?.position.y ?? 400, to?.position.y ?? 400),
244
+ };
245
+ const chatSessions = new Map(get().chatSessions);
246
+ chatSessions.set(sessionId, {
247
+ id: sessionId,
248
+ initiatorId: callerAgentId,
249
+ memberIds: [callerAgentId, event.to],
250
+ meetPoint,
251
+ duration: 30000,
252
+ startTime: Date.now(),
253
+ });
254
+ agents.set(callerAgentId, { ...agents.get(callerAgentId)!, chatSessionId: sessionId });
255
+ if (to) agents.set(to.id, { ...to, state: 'submitted', taskFrom: fromId, chatSessionId: sessionId });
256
+ set({ chatSessions });
257
+ }
258
+ break;
259
+ }
260
+ case 'task_status': {
261
+ const targetId = taskToAgent.get(event.taskId);
262
+ if (targetId) {
263
+ const agent = agents.get(targetId);
264
+ if (agent) {
265
+ let newState: AgentState;
266
+ switch (event.state) {
267
+ case 'submitted': newState = 'submitted'; break;
268
+ case 'working': newState = 'working'; break;
269
+ case 'completed': newState = 'completed'; break;
270
+ case 'failed': newState = 'failed'; break;
271
+ case 'canceled': newState = 'canceled'; break;
272
+ case 'rejected': newState = 'rejected'; break;
273
+ case 'input-required': newState = 'input-required'; break;
274
+ case 'auth-required': newState = 'auth-required'; break;
275
+ default: newState = 'idle';
276
+ }
277
+ const msg = event.message ?? agent.lastMessage;
278
+ agents.set(targetId, { ...agent, state: newState, lastMessage: msg });
279
+ }
280
+ }
281
+ const terminalStates = ['completed', 'failed', 'canceled', 'rejected'];
282
+ if (terminalStates.includes(event.state)) {
283
+ // Clear master bubble on task end
284
+ set({ masterMessage: null });
285
+
286
+ // End any peer chat session associated with this task
287
+ const peerSessionId = `peer-${event.taskId}`;
288
+ if (get().chatSessions.has(peerSessionId)) {
289
+ setTimeout(() => {
290
+ useMonitorStore.getState().endChatSession(peerSessionId);
291
+ }, 1500);
292
+ }
293
+
294
+ for (const [id, link] of links) {
295
+ if (link.taskId === event.taskId) {
296
+ setTimeout(() => {
297
+ const s = useMonitorStore.getState();
298
+ const newLinks = new Map(s.links);
299
+ newLinks.delete(id);
300
+ const newAgents = new Map(s.agents);
301
+ if (targetId) {
302
+ const a = newAgents.get(targetId);
303
+ if (a && (a.state === 'completed' || a.state === 'failed' || a.state === 'canceled' || a.state === 'rejected')) {
304
+ newAgents.set(targetId, { ...a, state: 'idle', lastToolName: undefined, chatSessionId: undefined, lastMessage: undefined, taskFrom: undefined });
305
+ }
306
+ }
307
+ useMonitorStore.setState({ links: newLinks, agents: newAgents });
308
+ }, 3000);
309
+ }
310
+ }
311
+ setTimeout(() => {
312
+ const newMap = new Map(useMonitorStore.getState().taskToAgent);
313
+ newMap.delete(event.taskId);
314
+ useMonitorStore.setState({ taskToAgent: newMap });
315
+ }, 5000);
316
+ }
317
+ break;
318
+ }
319
+ case 'tool_call_start': {
320
+ const targetId = taskToAgent.get(event.taskId);
321
+ if (targetId) {
322
+ const agent = agents.get(targetId);
323
+ if (agent) agents.set(targetId, { ...agent, state: 'tool_call', lastToolName: event.toolName });
324
+ }
325
+ break;
326
+ }
327
+ case 'tool_call_end': {
328
+ const targetId = taskToAgent.get(event.taskId);
329
+ if (targetId) {
330
+ const agent = agents.get(targetId);
331
+ if (agent && agent.state === 'tool_call') agents.set(targetId, { ...agent, state: 'working', lastToolName: undefined });
332
+ }
333
+ break;
334
+ }
335
+ case 'chat_response': {
336
+ const chat = get().userChat;
337
+ if (chat && chat.agentId === event.agentId) {
338
+ const newChat = {
339
+ ...chat,
340
+ messages: [...chat.messages, { role: 'agent' as const, text: event.text }],
341
+ loading: event.final ? false : chat.loading,
342
+ };
343
+ set({ userChat: newChat });
344
+ }
345
+ break;
346
+ }
347
+ }
348
+
349
+ set({ agents, links, taskToAgent });
350
+ scheduleSave(agents);
351
+ },
352
+ })});
@@ -0,0 +1,72 @@
1
+ export type MonitorEvent =
2
+ | { type: 'agent_loaded'; agentId: string; position?: { x: number; y: number } }
3
+ | { type: 'agent_reloaded'; agentId: string }
4
+ | { type: 'task_created'; taskId: string; contextId: string; from: string; to: string; preview: string }
5
+ | { type: 'task_status'; taskId: string; state: string; message?: string }
6
+ | { type: 'tool_call_start'; taskId: string; toolName: string; inputSummary: string }
7
+ | { type: 'tool_call_end'; taskId: string; toolName: string; success: boolean }
8
+ | { type: 'chat_response'; chatId: string; agentId: string; text: string; final: boolean };
9
+
10
+ export type AgentState =
11
+ | 'idle'
12
+ | 'submitted' // task just received, walking to master
13
+ | 'working' // executing task (attack or chat depending on context)
14
+ | 'tool_call' // working sub-state: calling a tool
15
+ | 'input-required' // paused, waiting for client input (walk back to master)
16
+ | 'auth-required' // paused, waiting for auth (same as input-required)
17
+ | 'completed' // task done, walking home
18
+ | 'failed' // task failed, shake-hand then walk home
19
+ | 'canceled' // task canceled by client, walk home
20
+ | 'rejected'; // agent refused the task, shake head at master
21
+
22
+ export type AgentAnim = 'idle' | 'walk' | 'sit' | 'attack' | 'chat' | 'daydream' | 'shake_hand' | 'head_shake';
23
+
24
+ /**
25
+ * A chat session between N agents.
26
+ * - initiatorId creates the session and invites others
27
+ * - memberIds includes all participants (initiator + invitees)
28
+ * - meetPoint is where everyone walks to
29
+ * - Each member gets a position offset around the meetPoint (computed from index)
30
+ */
31
+ export interface ChatSession {
32
+ id: string;
33
+ initiatorId: string;
34
+ memberIds: string[];
35
+ meetPoint: { x: number; y: number };
36
+ duration: number;
37
+ startTime: number;
38
+ }
39
+
40
+ export interface Agent {
41
+ id: string;
42
+ position: { x: number; y: number };
43
+ homePosition: { x: number; y: number };
44
+ stakePosition: { x: number; y: number };
45
+ sprite: string;
46
+ state: AgentState;
47
+ currentAnim: AgentAnim;
48
+ chatSessionId?: string; // ID of the chat session this agent is in
49
+ taskFrom?: string; // 'user' for client calls, agent-id for agent-to-agent calls
50
+ lastToolName?: string;
51
+ lastMessage?: string;
52
+ }
53
+
54
+ export interface MessageLink {
55
+ id: string;
56
+ from: string;
57
+ to: string;
58
+ preview: string;
59
+ startTime: number;
60
+ taskId: string;
61
+ }
62
+
63
+ export interface ChatMessage {
64
+ role: 'user' | 'agent';
65
+ text: string;
66
+ }
67
+
68
+ export interface UserChat {
69
+ agentId: string;
70
+ messages: ChatMessage[];
71
+ loading: boolean;
72
+ }
@@ -0,0 +1,66 @@
1
+ import { useMonitorStore } from './store.js';
2
+ import type { MonitorEvent } from './types.js';
3
+
4
+ const WS_URL = import.meta.env.VITE_MONITOR_WS_URL || 'ws://localhost:41242/monitor';
5
+
6
+ /** Shared persistent connection for both events and commands. */
7
+ let sharedWs: WebSocket | null = null;
8
+
9
+ export function connectWs(): () => void {
10
+ const ws = new WebSocket(WS_URL);
11
+ sharedWs = ws;
12
+
13
+ ws.addEventListener('open', () => {
14
+ useMonitorStore.setState({ connected: true });
15
+ });
16
+ ws.addEventListener('close', () => {
17
+ // Only clear sharedWs if it's still pointing to this instance
18
+ // (avoids race with StrictMode double-mount)
19
+ if (sharedWs === ws) {
20
+ sharedWs = null;
21
+ }
22
+ useMonitorStore.setState({ connected: false });
23
+ });
24
+ ws.addEventListener('error', () => {
25
+ useMonitorStore.setState({ connected: false });
26
+ });
27
+ ws.addEventListener('message', (e) => {
28
+ try {
29
+ const event = JSON.parse(e.data) as MonitorEvent;
30
+ useMonitorStore.getState().applyEvent(event);
31
+ } catch (err) {
32
+ console.error('[ws-client] bad message:', err);
33
+ }
34
+ });
35
+
36
+ return () => ws.close();
37
+ }
38
+
39
+ export function sendCommand(cmd: object): void {
40
+ if (sharedWs && sharedWs.readyState === WebSocket.OPEN) {
41
+ sharedWs.send(JSON.stringify(cmd));
42
+ return;
43
+ }
44
+ // Fallback: open a one-shot connection
45
+ const ws = new WebSocket(WS_URL);
46
+ ws.addEventListener('open', () => {
47
+ ws.send(JSON.stringify(cmd));
48
+ ws.close();
49
+ });
50
+ }
51
+
52
+ let chatIdCounter = 0;
53
+
54
+ /** Send a chat message to an agent and receive the response via store events. */
55
+ export function sendChatMessage(agentId: string, message: string): string {
56
+ const chatId = `chat-${Date.now()}-${++chatIdCounter}`;
57
+ useMonitorStore.getState().addChatMessage({ role: 'user', text: message });
58
+ useMonitorStore.getState().setChatLoading(true);
59
+ sendCommand({ type: 'chat', chatId, agentId, message });
60
+ return chatId;
61
+ }
62
+
63
+ // Expose for E2E testing
64
+ if (typeof window !== 'undefined') {
65
+ (window as any).__DOJO_SEND_CHAT__ = sendChatMessage;
66
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { App } from './App.js';
4
+
5
+ createRoot(document.getElementById('root')!).render(
6
+ <React.StrictMode>
7
+ <App />
8
+ </React.StrictMode>
9
+ );
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173,
8
+ proxy: {
9
+ // Optional: proxy A2A HTTP calls to framework (if needed)
10
+ // '/a2a': 'http://localhost:41241',
11
+ },
12
+ },
13
+ });