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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/CLAUDE.md +112 -44
- data/Gemfile.lock +31 -29
- data/README.md +5 -0
- data/frontend/index.html +5 -1
- data/frontend/package-lock.json +123 -0
- data/frontend/package.json +4 -0
- data/frontend/src/App.tsx +70 -71
- data/frontend/src/canvas/CanvasView.tsx +113 -0
- data/frontend/src/canvas/ForceLayout.ts +115 -0
- data/frontend/src/canvas/SceneManager.ts +587 -0
- data/frontend/src/canvas/canvasStore.ts +47 -0
- data/frontend/src/canvas/constants.ts +95 -0
- data/frontend/src/canvas/controls/CameraControls.ts +98 -0
- data/frontend/src/canvas/edges/MessageArc.ts +149 -0
- data/frontend/src/canvas/inspector/InspectorPanel.tsx +31 -0
- data/frontend/src/canvas/inspector/POInspector.tsx +262 -0
- data/frontend/src/canvas/inspector/ToolCallInspector.tsx +67 -0
- data/frontend/src/canvas/nodes/PONode.ts +249 -0
- data/frontend/src/canvas/nodes/ToolCallNode.ts +116 -0
- data/frontend/src/canvas/types.ts +24 -0
- data/frontend/src/components/ContextMenu.tsx +5 -4
- data/frontend/src/components/Inspector.tsx +232 -0
- data/frontend/src/components/MarkdownMessage.tsx +22 -20
- data/frontend/src/components/MethodList.tsx +90 -0
- data/frontend/src/components/ModelSelector.tsx +13 -14
- data/frontend/src/components/NotificationPanel.tsx +29 -33
- data/frontend/src/components/ObjectList.tsx +78 -0
- data/frontend/src/components/PaneSlot.tsx +30 -0
- data/frontend/src/components/SourcePane.tsx +202 -0
- data/frontend/src/components/SystemBar.tsx +74 -0
- data/frontend/src/components/Transcript.tsx +76 -0
- data/frontend/src/components/UsagePanel.tsx +27 -27
- data/frontend/src/components/Workspace.tsx +260 -0
- data/frontend/src/components/index.ts +10 -9
- data/frontend/src/hooks/useResize.ts +55 -0
- data/frontend/src/hooks/useWebSocket.ts +274 -189
- data/frontend/src/index.css +69 -3
- data/frontend/src/store/index.ts +23 -0
- data/frontend/src/types/index.ts +5 -0
- data/frontend/tailwind.config.js +28 -9
- data/lib/prompt_objects/capability.rb +23 -1
- data/lib/prompt_objects/connectors/mcp.rb +5 -22
- data/lib/prompt_objects/environment.rb +8 -0
- data/lib/prompt_objects/llm/openai_adapter.rb +22 -0
- data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +1 -2
- data/lib/prompt_objects/mcp/tools/inspect_po.rb +1 -31
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +2 -8
- data/lib/prompt_objects/primitives/list_files.rb +1 -2
- data/lib/prompt_objects/prompt_object.rb +150 -6
- data/lib/prompt_objects/server/api/routes.rb +3 -48
- data/lib/prompt_objects/server/app.rb +9 -0
- data/lib/prompt_objects/server/public/assets/index-D1myxE0l.js +4345 -0
- data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.css +1 -0
- data/lib/prompt_objects/server/public/index.html +7 -3
- data/lib/prompt_objects/server/websocket_handler.rb +23 -100
- data/lib/prompt_objects/server.rb +6 -62
- data/prompt_objects.gemspec +1 -1
- data/templates/arc-agi-1/primitives/find_objects.rb +1 -1
- data/templates/arc-agi-1/primitives/grid_diff.rb +2 -2
- data/templates/arc-agi-1/primitives/grid_info.rb +1 -1
- data/templates/arc-agi-1/primitives/grid_transform.rb +1 -1
- data/templates/arc-agi-1/primitives/render_grid.rb +1 -0
- data/templates/arc-agi-1/primitives/test_solution.rb +3 -0
- metadata +26 -14
- data/frontend/src/components/CapabilitiesPanel.tsx +0 -141
- data/frontend/src/components/ChatPanel.tsx +0 -288
- data/frontend/src/components/Dashboard.tsx +0 -83
- data/frontend/src/components/Header.tsx +0 -141
- data/frontend/src/components/MessageBus.tsx +0 -56
- data/frontend/src/components/POCard.tsx +0 -56
- data/frontend/src/components/PODetail.tsx +0 -124
- data/frontend/src/components/PromptPanel.tsx +0 -156
- data/frontend/src/components/SessionsPanel.tsx +0 -174
- data/frontend/src/components/ThreadsSidebar.tsx +0 -163
- data/lib/prompt_objects/server/public/assets/index-Bkme6COu.css +0 -1
- 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
|
-
|
|
288
|
+
const socket = new WebSocket(wsUrl)
|
|
289
|
+
ws.current = socket
|
|
53
290
|
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
326
|
+
socket.onerror = (error) => {
|
|
327
|
+
if (ws.current !== socket) return
|
|
77
328
|
console.error('WebSocket error:', error)
|
|
78
329
|
}
|
|
79
330
|
|
|
80
|
-
|
|
331
|
+
socket.onmessage = (event) => {
|
|
332
|
+
if (ws.current !== socket) return
|
|
81
333
|
try {
|
|
82
334
|
const message: WSMessage = JSON.parse(event.data)
|
|
83
|
-
|
|
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
|
-
|
|
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
|
|
data/frontend/src/index.css
CHANGED
|
@@ -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:
|
|
17
|
-
height:
|
|
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-
|
|
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
|
+
}
|
data/frontend/src/store/index.ts
CHANGED
|
@@ -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: [],
|
data/frontend/src/types/index.ts
CHANGED
|
@@ -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
|
|