prompt_objects 0.3.1 → 0.5.0

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/CLAUDE.md +112 -44
  4. data/Gemfile.lock +31 -29
  5. data/README.md +5 -0
  6. data/frontend/index.html +5 -1
  7. data/frontend/package-lock.json +123 -0
  8. data/frontend/package.json +4 -0
  9. data/frontend/src/App.tsx +70 -71
  10. data/frontend/src/canvas/CanvasView.tsx +113 -0
  11. data/frontend/src/canvas/ForceLayout.ts +115 -0
  12. data/frontend/src/canvas/SceneManager.ts +587 -0
  13. data/frontend/src/canvas/canvasStore.ts +47 -0
  14. data/frontend/src/canvas/constants.ts +95 -0
  15. data/frontend/src/canvas/controls/CameraControls.ts +98 -0
  16. data/frontend/src/canvas/edges/MessageArc.ts +149 -0
  17. data/frontend/src/canvas/inspector/InspectorPanel.tsx +31 -0
  18. data/frontend/src/canvas/inspector/POInspector.tsx +262 -0
  19. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +67 -0
  20. data/frontend/src/canvas/nodes/PONode.ts +249 -0
  21. data/frontend/src/canvas/nodes/ToolCallNode.ts +116 -0
  22. data/frontend/src/canvas/types.ts +24 -0
  23. data/frontend/src/components/ContextMenu.tsx +5 -4
  24. data/frontend/src/components/Inspector.tsx +232 -0
  25. data/frontend/src/components/MarkdownMessage.tsx +22 -20
  26. data/frontend/src/components/MethodList.tsx +90 -0
  27. data/frontend/src/components/ModelSelector.tsx +13 -14
  28. data/frontend/src/components/NotificationPanel.tsx +29 -33
  29. data/frontend/src/components/ObjectList.tsx +78 -0
  30. data/frontend/src/components/PaneSlot.tsx +30 -0
  31. data/frontend/src/components/SourcePane.tsx +202 -0
  32. data/frontend/src/components/SystemBar.tsx +74 -0
  33. data/frontend/src/components/Transcript.tsx +76 -0
  34. data/frontend/src/components/UsagePanel.tsx +27 -27
  35. data/frontend/src/components/Workspace.tsx +260 -0
  36. data/frontend/src/components/index.ts +10 -9
  37. data/frontend/src/hooks/useResize.ts +55 -0
  38. data/frontend/src/hooks/useWebSocket.ts +274 -189
  39. data/frontend/src/index.css +69 -3
  40. data/frontend/src/store/index.ts +23 -0
  41. data/frontend/src/types/index.ts +5 -0
  42. data/frontend/tailwind.config.js +28 -9
  43. data/lib/prompt_objects/capability.rb +23 -1
  44. data/lib/prompt_objects/connectors/mcp.rb +5 -22
  45. data/lib/prompt_objects/environment.rb +8 -0
  46. data/lib/prompt_objects/llm/openai_adapter.rb +22 -0
  47. data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +1 -2
  48. data/lib/prompt_objects/mcp/tools/inspect_po.rb +1 -31
  49. data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +2 -8
  50. data/lib/prompt_objects/primitives/list_files.rb +1 -2
  51. data/lib/prompt_objects/prompt_object.rb +150 -6
  52. data/lib/prompt_objects/server/api/routes.rb +3 -48
  53. data/lib/prompt_objects/server/app.rb +9 -0
  54. data/lib/prompt_objects/server/public/assets/index-D1myxE0l.js +4345 -0
  55. data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.css +1 -0
  56. data/lib/prompt_objects/server/public/index.html +7 -3
  57. data/lib/prompt_objects/server/websocket_handler.rb +23 -100
  58. data/lib/prompt_objects/server.rb +6 -62
  59. data/prompt_objects.gemspec +1 -1
  60. data/templates/arc-agi-1/primitives/find_objects.rb +1 -1
  61. data/templates/arc-agi-1/primitives/grid_diff.rb +2 -2
  62. data/templates/arc-agi-1/primitives/grid_info.rb +1 -1
  63. data/templates/arc-agi-1/primitives/grid_transform.rb +1 -1
  64. data/templates/arc-agi-1/primitives/render_grid.rb +1 -0
  65. data/templates/arc-agi-1/primitives/test_solution.rb +3 -0
  66. metadata +26 -14
  67. data/frontend/src/components/CapabilitiesPanel.tsx +0 -141
  68. data/frontend/src/components/ChatPanel.tsx +0 -288
  69. data/frontend/src/components/Dashboard.tsx +0 -83
  70. data/frontend/src/components/Header.tsx +0 -141
  71. data/frontend/src/components/MessageBus.tsx +0 -56
  72. data/frontend/src/components/POCard.tsx +0 -56
  73. data/frontend/src/components/PODetail.tsx +0 -124
  74. data/frontend/src/components/PromptPanel.tsx +0 -156
  75. data/frontend/src/components/SessionsPanel.tsx +0 -174
  76. data/frontend/src/components/ThreadsSidebar.tsx +0 -163
  77. data/lib/prompt_objects/server/public/assets/index-Bkme6COu.css +0 -1
  78. data/lib/prompt_objects/server/public/assets/index-CQ7lVDF_.js +0 -77
@@ -19,9 +19,15 @@ import type {
19
19
  export function useWebSocket() {
20
20
  const ws = useRef<WebSocket | null>(null)
21
21
  const reconnectTimeout = useRef<number | null>(null)
22
+ const disposed = useRef(false)
23
+
24
+ // Use a ref for the message handler so the WS always calls the latest version,
25
+ // regardless of when `connect()` was created or how many reconnections happened.
26
+ const handleMessageRef = useRef<(message: WSMessage) => void>(() => {})
22
27
 
23
28
  const {
24
29
  setConnected,
30
+ resetOnDisconnect,
25
31
  setEnvironment,
26
32
  setPromptObject,
27
33
  removePromptObject,
@@ -39,7 +45,237 @@ export function useWebSocket() {
39
45
  setUsageData,
40
46
  } = useStore()
41
47
 
48
+ // Keep the handler ref up to date every render
49
+ handleMessageRef.current = (message: WSMessage) => {
50
+ switch (message.type) {
51
+ case 'environment':
52
+ setEnvironment(message.payload as Environment)
53
+ break
54
+
55
+ case 'po_state': {
56
+ const { name, state } = message.payload as {
57
+ name: string
58
+ state: Partial<PromptObject>
59
+ }
60
+ setPromptObject(name, state)
61
+ break
62
+ }
63
+
64
+ case 'po_response': {
65
+ const { target, content } = message.payload as {
66
+ target: string
67
+ content: string
68
+ }
69
+ setPendingResponse(target, content)
70
+ // Clear after a short delay to allow UI to update
71
+ setTimeout(() => clearPendingResponse(target), 100)
72
+ break
73
+ }
74
+
75
+ case 'stream': {
76
+ const { target, chunk } = message.payload as {
77
+ target: string
78
+ chunk: string
79
+ }
80
+ appendStreamChunk(target, chunk)
81
+ break
82
+ }
83
+
84
+ case 'stream_end': {
85
+ const { target } = message.payload as { target: string }
86
+ clearStream(target)
87
+ break
88
+ }
89
+
90
+ case 'bus_message':
91
+ addBusMessage(message.payload as BusMessage)
92
+ break
93
+
94
+ case 'notification':
95
+ addNotification(message.payload as Notification)
96
+ break
97
+
98
+ case 'notification_resolved': {
99
+ const { id } = message.payload as { id: string }
100
+ removeNotification(id)
101
+ break
102
+ }
103
+
104
+ // Live file updates
105
+ case 'po_added': {
106
+ const { name, state } = message.payload as {
107
+ name: string
108
+ state: Partial<PromptObject>
109
+ }
110
+ console.log('PO added:', name)
111
+ setPromptObject(name, state)
112
+ break
113
+ }
114
+
115
+ case 'po_modified': {
116
+ const { name, state } = message.payload as {
117
+ name: string
118
+ state: Partial<PromptObject>
119
+ }
120
+ console.log('PO modified:', name)
121
+ setPromptObject(name, state)
122
+ break
123
+ }
124
+
125
+ case 'po_removed': {
126
+ const { name } = message.payload as { name: string }
127
+ console.log('PO removed:', name)
128
+ removePromptObject(name)
129
+ break
130
+ }
131
+
132
+ case 'po_delegation_started': {
133
+ const { target, caller } = message.payload as {
134
+ target: string
135
+ caller: string
136
+ thread_id: string
137
+ tool_call_id: string
138
+ }
139
+ // Mark the target PO as delegated — it's now working on behalf of caller
140
+ setPromptObject(target, { status: 'thinking', delegated_by: caller })
141
+ break
142
+ }
143
+
144
+ case 'po_delegation_completed': {
145
+ const { target } = message.payload as {
146
+ target: string
147
+ caller: string
148
+ thread_id: string
149
+ tool_call_id: string
150
+ }
151
+ // Delegation finished — target PO returns to idle
152
+ setPromptObject(target, { status: 'idle', delegated_by: null })
153
+ break
154
+ }
155
+
156
+ case 'session_updated': {
157
+ const { target, session_id, messages } = message.payload as {
158
+ target: string
159
+ session_id: string
160
+ messages: Message[]
161
+ }
162
+ updateSessionMessages(target, session_id, messages)
163
+ break
164
+ }
165
+
166
+ case 'thread_created': {
167
+ const { target, thread_id, thread_type } = message.payload as {
168
+ target: string
169
+ thread_id: string
170
+ name: string | null
171
+ thread_type: ThreadType
172
+ }
173
+ console.log('Thread created:', target, thread_id, thread_type)
174
+ // IMMEDIATELY switch to the new thread so user sees their message
175
+ // This ensures session_updated messages for this thread are displayed
176
+ switchPOSession(target, thread_id)
177
+ break
178
+ }
179
+
180
+ case 'thread_tree': {
181
+ // Thread tree response - could be used for navigation
182
+ console.log('Thread tree received:', message.payload)
183
+ break
184
+ }
185
+
186
+ case 'llm_config':
187
+ setLLMConfig(message.payload as LLMConfig)
188
+ break
189
+
190
+ case 'llm_switched': {
191
+ const { provider, model } = message.payload as {
192
+ provider: string
193
+ model: string
194
+ }
195
+ updateCurrentLLM(provider, model)
196
+ break
197
+ }
198
+
199
+ case 'session_usage': {
200
+ setUsageData(message.payload as Record<string, unknown>)
201
+ break
202
+ }
203
+
204
+ case 'thread_export': {
205
+ const { content, format, session_id } = message.payload as { content: string; format: string; session_id: string }
206
+ const mimeType = format === 'json' ? 'application/json' : 'text/markdown'
207
+ const ext = format === 'json' ? 'json' : 'md'
208
+ const blob = new Blob([content], { type: mimeType })
209
+ const url = URL.createObjectURL(blob)
210
+ const a = document.createElement('a')
211
+ a.href = url
212
+ a.download = `${session_id}.${ext}`
213
+ a.click()
214
+ URL.revokeObjectURL(url)
215
+ break
216
+ }
217
+
218
+ case 'prompt_updated': {
219
+ const { target, success } = message.payload as { target: string; success: boolean }
220
+ if (!success) console.warn('Prompt update failed for:', target)
221
+ break
222
+ }
223
+
224
+ case 'llm_error': {
225
+ const { po_name, error, provider, model } = message.payload as {
226
+ po_name: string
227
+ provider: string
228
+ model: string
229
+ error: string
230
+ error_class: string
231
+ }
232
+ console.error(`LLM error for ${po_name} (${provider}/${model}):`, error)
233
+ // Reset PO to idle so UI isn't stuck in "thinking" state
234
+ setPromptObject(po_name, { status: 'idle' })
235
+ break
236
+ }
237
+
238
+ case 'session_created':
239
+ // Session creation confirmed — po_state update follows with full state
240
+ break
241
+
242
+ case 'session_switched':
243
+ // Session switch confirmed — po_state update follows with full state
244
+ break
245
+
246
+ case 'error': {
247
+ const { message: errorMsg } = message.payload as { message: string }
248
+ console.error('Server error:', errorMsg)
249
+ break
250
+ }
251
+
252
+ case 'pong':
253
+ // Heartbeat response, ignore
254
+ break
255
+
256
+ default:
257
+ console.log('Unknown message type:', message.type)
258
+ }
259
+ }
260
+
42
261
  const connect = useCallback(() => {
262
+ // Don't reconnect if we've been disposed (component unmounted)
263
+ if (disposed.current) return
264
+
265
+ // Close any existing connection before creating a new one
266
+ // to prevent duplicate connections racing each other
267
+ if (ws.current) {
268
+ // Remove handlers first so the close doesn't trigger another reconnect
269
+ ws.current.onclose = null
270
+ ws.current.onerror = null
271
+ ws.current.onmessage = null
272
+ ws.current.onopen = null
273
+ if (ws.current.readyState === WebSocket.OPEN || ws.current.readyState === WebSocket.CONNECTING) {
274
+ ws.current.close()
275
+ }
276
+ ws.current = null
277
+ }
278
+
43
279
  // Determine WebSocket URL
44
280
  // In dev mode (Vite on 5173), connect directly to Ruby server on 3000
45
281
  // In production, connect to same host
@@ -49,9 +285,12 @@ export function useWebSocket() {
49
285
  const wsUrl = `${protocol}//${host}`
50
286
 
51
287
  console.log('Connecting to WebSocket:', wsUrl)
52
- ws.current = new WebSocket(wsUrl)
288
+ const socket = new WebSocket(wsUrl)
289
+ ws.current = socket
53
290
 
54
- ws.current.onopen = () => {
291
+ socket.onopen = () => {
292
+ // Guard against stale handler from a replaced socket
293
+ if (ws.current !== socket) return
55
294
  console.log('WebSocket connected')
56
295
  setConnected(true)
57
296
 
@@ -62,219 +301,65 @@ export function useWebSocket() {
62
301
  }
63
302
  }
64
303
 
65
- ws.current.onclose = () => {
304
+ socket.onclose = () => {
305
+ // Guard: if this socket was already replaced, ignore its close event.
306
+ // This prevents zombie onclose handlers from triggering reconnects
307
+ // after a fresh connection has already been established.
308
+ if (ws.current !== socket) return
309
+ if (disposed.current) return
310
+
66
311
  console.log('WebSocket disconnected')
67
- setConnected(false)
312
+ // Reset PO statuses to idle and clear broken streams —
313
+ // prevents locked chat input and stale streaming indicators
314
+ resetOnDisconnect()
68
315
 
69
316
  // Attempt to reconnect after 2 seconds
317
+ if (reconnectTimeout.current) {
318
+ clearTimeout(reconnectTimeout.current)
319
+ }
70
320
  reconnectTimeout.current = window.setTimeout(() => {
71
321
  console.log('Attempting to reconnect...')
72
322
  connect()
73
323
  }, 2000)
74
324
  }
75
325
 
76
- ws.current.onerror = (error) => {
326
+ socket.onerror = (error) => {
327
+ if (ws.current !== socket) return
77
328
  console.error('WebSocket error:', error)
78
329
  }
79
330
 
80
- ws.current.onmessage = (event) => {
331
+ socket.onmessage = (event) => {
332
+ if (ws.current !== socket) return
81
333
  try {
82
334
  const message: WSMessage = JSON.parse(event.data)
83
- handleMessage(message)
335
+ // Always call the latest handler via ref — no stale closures
336
+ handleMessageRef.current(message)
84
337
  } catch (error) {
85
338
  console.error('Failed to parse WebSocket message:', error)
86
339
  }
87
340
  }
88
- }, [setConnected])
89
-
90
- const handleMessage = useCallback(
91
- (message: WSMessage) => {
92
- switch (message.type) {
93
- case 'environment':
94
- setEnvironment(message.payload as Environment)
95
- break
96
-
97
- case 'po_state': {
98
- const { name, state } = message.payload as {
99
- name: string
100
- state: Partial<PromptObject>
101
- }
102
- setPromptObject(name, state)
103
- break
104
- }
105
-
106
- case 'po_response': {
107
- const { target, content } = message.payload as {
108
- target: string
109
- content: string
110
- }
111
- setPendingResponse(target, content)
112
- // Clear after a short delay to allow UI to update
113
- setTimeout(() => clearPendingResponse(target), 100)
114
- break
115
- }
116
-
117
- case 'stream': {
118
- const { target, chunk } = message.payload as {
119
- target: string
120
- chunk: string
121
- }
122
- appendStreamChunk(target, chunk)
123
- break
124
- }
125
-
126
- case 'stream_end': {
127
- const { target } = message.payload as { target: string }
128
- clearStream(target)
129
- break
130
- }
131
-
132
- case 'bus_message':
133
- addBusMessage(message.payload as BusMessage)
134
- break
135
-
136
- case 'notification':
137
- addNotification(message.payload as Notification)
138
- break
139
-
140
- case 'notification_resolved': {
141
- const { id } = message.payload as { id: string }
142
- removeNotification(id)
143
- break
144
- }
145
-
146
- // Live file updates
147
- case 'po_added': {
148
- const { name, state } = message.payload as {
149
- name: string
150
- state: Partial<PromptObject>
151
- }
152
- console.log('PO added:', name)
153
- setPromptObject(name, state)
154
- break
155
- }
156
-
157
- case 'po_modified': {
158
- const { name, state } = message.payload as {
159
- name: string
160
- state: Partial<PromptObject>
161
- }
162
- console.log('PO modified:', name)
163
- setPromptObject(name, state)
164
- break
165
- }
166
-
167
- case 'po_removed': {
168
- const { name } = message.payload as { name: string }
169
- console.log('PO removed:', name)
170
- removePromptObject(name)
171
- break
172
- }
173
-
174
- case 'session_updated': {
175
- const { target, session_id, messages } = message.payload as {
176
- target: string
177
- session_id: string
178
- messages: Message[]
179
- }
180
- updateSessionMessages(target, session_id, messages)
181
- break
182
- }
183
-
184
- case 'thread_created': {
185
- const { target, thread_id, thread_type } = message.payload as {
186
- target: string
187
- thread_id: string
188
- name: string | null
189
- thread_type: ThreadType
190
- }
191
- console.log('Thread created:', target, thread_id, thread_type)
192
- // IMMEDIATELY switch to the new thread so user sees their message
193
- // This ensures session_updated messages for this thread are displayed
194
- switchPOSession(target, thread_id)
195
- break
196
- }
197
-
198
- case 'thread_tree': {
199
- // Thread tree response - could be used for navigation
200
- console.log('Thread tree received:', message.payload)
201
- break
202
- }
341
+ }, [setConnected, resetOnDisconnect])
203
342
 
204
- case 'llm_config':
205
- setLLMConfig(message.payload as LLMConfig)
206
- break
207
-
208
- case 'llm_switched': {
209
- const { provider, model } = message.payload as {
210
- provider: string
211
- model: string
212
- }
213
- updateCurrentLLM(provider, model)
214
- break
215
- }
216
-
217
- case 'session_usage': {
218
- setUsageData(message.payload as Record<string, unknown>)
219
- break
220
- }
221
-
222
- case 'thread_export': {
223
- const { content, format, session_id } = message.payload as { content: string; format: string; session_id: string }
224
- const mimeType = format === 'json' ? 'application/json' : 'text/markdown'
225
- const ext = format === 'json' ? 'json' : 'md'
226
- const blob = new Blob([content], { type: mimeType })
227
- const url = URL.createObjectURL(blob)
228
- const a = document.createElement('a')
229
- a.href = url
230
- a.download = `${session_id}.${ext}`
231
- a.click()
232
- URL.revokeObjectURL(url)
233
- break
234
- }
235
-
236
- case 'error': {
237
- const { message: errorMsg } = message.payload as { message: string }
238
- console.error('Server error:', errorMsg)
239
- break
240
- }
241
-
242
- case 'pong':
243
- // Heartbeat response, ignore
244
- break
245
-
246
- default:
247
- console.log('Unknown message type:', message.type)
248
- }
249
- },
250
- [
251
- setEnvironment,
252
- setPromptObject,
253
- removePromptObject,
254
- updateSessionMessages,
255
- switchPOSession,
256
- setPendingResponse,
257
- clearPendingResponse,
258
- appendStreamChunk,
259
- clearStream,
260
- addBusMessage,
261
- addNotification,
262
- removeNotification,
263
- setLLMConfig,
264
- updateCurrentLLM,
265
- setUsageData,
266
- ]
267
- )
268
-
269
- // Connect on mount
343
+ // Connect on mount, clean up on unmount
270
344
  useEffect(() => {
345
+ disposed.current = false
271
346
  connect()
272
347
 
273
348
  return () => {
349
+ disposed.current = true
274
350
  if (reconnectTimeout.current) {
275
351
  clearTimeout(reconnectTimeout.current)
352
+ reconnectTimeout.current = null
353
+ }
354
+ if (ws.current) {
355
+ // Remove handlers before closing to prevent zombie events
356
+ ws.current.onclose = null
357
+ ws.current.onerror = null
358
+ ws.current.onmessage = null
359
+ ws.current.onopen = null
360
+ ws.current.close()
361
+ ws.current = null
276
362
  }
277
- ws.current?.close()
278
363
  }
279
364
  }, [connect])
280
365
 
@@ -5,6 +5,7 @@
5
5
  /* Base styles */
6
6
  html {
7
7
  color-scheme: dark;
8
+ font-size: 14px;
8
9
  }
9
10
 
10
11
  body {
@@ -13,8 +14,8 @@ body {
13
14
 
14
15
  /* Custom scrollbar */
15
16
  ::-webkit-scrollbar {
16
- width: 8px;
17
- height: 8px;
17
+ width: 6px;
18
+ height: 6px;
18
19
  }
19
20
 
20
21
  ::-webkit-scrollbar-track {
@@ -26,12 +27,77 @@ body {
26
27
  }
27
28
 
28
29
  ::-webkit-scrollbar-thumb:hover {
29
- @apply bg-po-accent;
30
+ @apply bg-po-border-focus;
31
+ }
32
+
33
+ /* Selection */
34
+ ::selection {
35
+ background: rgba(212, 149, 42, 0.3);
30
36
  }
31
37
 
32
38
  /* Custom utilities */
33
39
  @layer utilities {
34
40
  .scrollbar-thin {
35
41
  scrollbar-width: thin;
42
+ scrollbar-color: #3d3a37 #1a1918;
36
43
  }
37
44
  }
45
+
46
+ /* Resize handles */
47
+ .resize-handle {
48
+ @apply w-1 cursor-col-resize hover:bg-po-accent/30 active:bg-po-accent/50 transition-colors;
49
+ }
50
+
51
+ .resize-handle-h {
52
+ @apply h-1 cursor-row-resize hover:bg-po-accent/30 active:bg-po-accent/50 transition-colors;
53
+ }
54
+
55
+ /* Canvas CSS2DRenderer labels */
56
+ .canvas-node-label {
57
+ text-align: center;
58
+ pointer-events: none;
59
+ user-select: none;
60
+ }
61
+
62
+ .canvas-node-name {
63
+ display: block;
64
+ color: #e8e2da;
65
+ font-size: 12px;
66
+ font-weight: 500;
67
+ font-family: 'Geist Mono', 'IBM Plex Mono', monospace;
68
+ text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
69
+ }
70
+
71
+ .canvas-node-status {
72
+ display: block;
73
+ color: #78726a;
74
+ font-size: 10px;
75
+ font-family: 'Geist Mono', 'IBM Plex Mono', monospace;
76
+ text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
77
+ }
78
+
79
+ .canvas-node-badge {
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ width: 20px;
84
+ height: 20px;
85
+ background: #d4952a;
86
+ color: #1a1918;
87
+ font-size: 10px;
88
+ font-weight: 700;
89
+ border-radius: 50%;
90
+ pointer-events: none;
91
+ user-select: none;
92
+ box-shadow: 0 0 8px rgba(212, 149, 42, 0.6);
93
+ }
94
+
95
+ .canvas-toolcall-label {
96
+ color: #3b9a6e;
97
+ font-size: 10px;
98
+ font-family: 'Geist Mono', 'IBM Plex Mono', monospace;
99
+ text-align: center;
100
+ pointer-events: none;
101
+ user-select: none;
102
+ text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
103
+ }
@@ -13,6 +13,7 @@ interface Store {
13
13
  // Connection state
14
14
  connected: boolean
15
15
  setConnected: (connected: boolean) => void
16
+ resetOnDisconnect: () => void
16
17
 
17
18
  // Environment
18
19
  environment: Environment | null
@@ -32,12 +33,16 @@ interface Store {
32
33
  selectPO: (name: string | null) => void
33
34
  activeTab: 'chat' | 'sessions' | 'capabilities' | 'prompt'
34
35
  setActiveTab: (tab: Store['activeTab']) => void
36
+ currentView: 'dashboard' | 'canvas'
37
+ setCurrentView: (view: Store['currentView']) => void
35
38
 
36
39
  // Message Bus
37
40
  busMessages: BusMessage[]
38
41
  addBusMessage: (message: BusMessage) => void
39
42
  busOpen: boolean
40
43
  toggleBus: () => void
44
+ topPaneCollapsed: boolean
45
+ toggleTopPane: () => void
41
46
 
42
47
  // Notifications
43
48
  notifications: Notification[]
@@ -69,6 +74,20 @@ export const useStore = create<Store>((set) => ({
69
74
  // Connection
70
75
  connected: false,
71
76
  setConnected: (connected) => set({ connected }),
77
+ resetOnDisconnect: () =>
78
+ set((s) => {
79
+ // Reset all PO statuses to idle (server will re-send correct status on reconnect)
80
+ const resetPOs: Record<string, PromptObject> = {}
81
+ for (const [name, po] of Object.entries(s.promptObjects)) {
82
+ resetPOs[name] = po.status !== 'idle' ? { ...po, status: 'idle' } : po
83
+ }
84
+ return {
85
+ connected: false,
86
+ promptObjects: resetPOs,
87
+ streamingContent: {}, // Clear broken streams
88
+ pendingResponse: {}, // Clear stale pending responses
89
+ }
90
+ }),
72
91
 
73
92
  // Environment
74
93
  environment: null,
@@ -175,6 +194,8 @@ export const useStore = create<Store>((set) => ({
175
194
  selectPO: (name) => set({ selectedPO: name, activeTab: 'chat' }),
176
195
  activeTab: 'chat',
177
196
  setActiveTab: (activeTab) => set({ activeTab }),
197
+ currentView: 'dashboard',
198
+ setCurrentView: (currentView) => set({ currentView }),
178
199
 
179
200
  // Message Bus
180
201
  busMessages: [],
@@ -184,6 +205,8 @@ export const useStore = create<Store>((set) => ({
184
205
  })),
185
206
  busOpen: false,
186
207
  toggleBus: () => set((s) => ({ busOpen: !s.busOpen })),
208
+ topPaneCollapsed: false,
209
+ toggleTopPane: () => set((s) => ({ topPaneCollapsed: !s.topPaneCollapsed })),
187
210
 
188
211
  // Notifications
189
212
  notifications: [],
@@ -62,6 +62,7 @@ export interface PromptObject {
62
62
  sessions: Session[]
63
63
  prompt?: string // The markdown body/prompt
64
64
  config?: Record<string, unknown> // The YAML frontmatter config
65
+ delegated_by?: string | null // Name of the PO that called this one (set by delegation events)
65
66
  }
66
67
 
67
68
  export interface BusMessage {
@@ -109,6 +110,8 @@ export type WSMessageType =
109
110
  | 'po_added'
110
111
  | 'po_modified'
111
112
  | 'po_removed'
113
+ | 'po_delegation_started'
114
+ | 'po_delegation_completed'
112
115
  | 'stream'
113
116
  | 'stream_end'
114
117
  | 'bus_message'
@@ -123,6 +126,8 @@ export type WSMessageType =
123
126
  | 'llm_switched'
124
127
  | 'session_usage'
125
128
  | 'thread_export'
129
+ | 'prompt_updated'
130
+ | 'llm_error'
126
131
  | 'error'
127
132
  | 'pong'
128
133