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
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js'
|
|
3
|
+
import { COLORS, CSS_COLORS, NODE, ANIMATION } from '../constants'
|
|
4
|
+
|
|
5
|
+
const vertexShader = /* glsl */ `
|
|
6
|
+
varying vec2 vUv;
|
|
7
|
+
void main() {
|
|
8
|
+
vUv = uv;
|
|
9
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
10
|
+
}
|
|
11
|
+
`
|
|
12
|
+
|
|
13
|
+
const fragmentShader = /* glsl */ `
|
|
14
|
+
uniform vec3 uColor;
|
|
15
|
+
uniform vec3 uGlowColor;
|
|
16
|
+
uniform float uGlowIntensity;
|
|
17
|
+
uniform float uTime;
|
|
18
|
+
uniform float uHovered;
|
|
19
|
+
uniform float uSelected;
|
|
20
|
+
|
|
21
|
+
varying vec2 vUv;
|
|
22
|
+
|
|
23
|
+
void main() {
|
|
24
|
+
// Distance from center (0 at center, 1 at edge)
|
|
25
|
+
float dist = length(vUv - 0.5) * 2.0;
|
|
26
|
+
|
|
27
|
+
// Base dark fill
|
|
28
|
+
vec3 base = uColor * 0.3;
|
|
29
|
+
|
|
30
|
+
// Edge glow
|
|
31
|
+
float edgeGlow = smoothstep(0.5, 1.0, dist) * uGlowIntensity;
|
|
32
|
+
// Animated pulse
|
|
33
|
+
float pulse = sin(uTime * ${ANIMATION.pulseSpeed.toFixed(1)}) * 0.15 + 0.85;
|
|
34
|
+
edgeGlow *= pulse;
|
|
35
|
+
|
|
36
|
+
vec3 color = mix(base, uGlowColor, edgeGlow);
|
|
37
|
+
|
|
38
|
+
// Hover brightening
|
|
39
|
+
color = mix(color, uGlowColor * 1.2, uHovered * 0.3);
|
|
40
|
+
|
|
41
|
+
// Selection brightening
|
|
42
|
+
color = mix(color, uGlowColor * 1.5, uSelected * 0.4);
|
|
43
|
+
|
|
44
|
+
// Alpha: solid center, slight transparency at edges
|
|
45
|
+
float alpha = smoothstep(1.0, 0.8, dist);
|
|
46
|
+
|
|
47
|
+
gl_FragColor = vec4(color, alpha);
|
|
48
|
+
}
|
|
49
|
+
`
|
|
50
|
+
|
|
51
|
+
type POStatus = 'idle' | 'thinking' | 'calling_tool'
|
|
52
|
+
|
|
53
|
+
const STATUS_COLORS: Record<POStatus, number> = {
|
|
54
|
+
idle: COLORS.statusIdle,
|
|
55
|
+
thinking: COLORS.statusThinking,
|
|
56
|
+
calling_tool: COLORS.statusCallingTool,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const STATUS_CSS_COLORS: Record<POStatus, string> = {
|
|
60
|
+
idle: CSS_COLORS.statusIdle,
|
|
61
|
+
thinking: CSS_COLORS.statusThinking,
|
|
62
|
+
calling_tool: CSS_COLORS.statusCallingTool,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Color for when this PO is being called by another PO (delegation)
|
|
66
|
+
const DELEGATED_COLOR = 0x5a8fc2 // steel blue (matches po-status-delegated)
|
|
67
|
+
const DELEGATED_CSS_COLOR = '#5a8fc2'
|
|
68
|
+
|
|
69
|
+
export class PONode {
|
|
70
|
+
readonly id: string
|
|
71
|
+
readonly group: THREE.Group
|
|
72
|
+
readonly mesh: THREE.Mesh
|
|
73
|
+
|
|
74
|
+
private material: THREE.ShaderMaterial
|
|
75
|
+
private statusRing: THREE.LineLoop
|
|
76
|
+
private statusRingMaterial: THREE.LineBasicMaterial
|
|
77
|
+
private label: CSS2DObject
|
|
78
|
+
private labelEl: HTMLDivElement
|
|
79
|
+
private nameEl: HTMLSpanElement
|
|
80
|
+
private statusEl: HTMLSpanElement
|
|
81
|
+
private badge: CSS2DObject
|
|
82
|
+
private badgeEl: HTMLDivElement
|
|
83
|
+
private badgeCountEl: HTMLSpanElement
|
|
84
|
+
|
|
85
|
+
private targetPosition = new THREE.Vector3()
|
|
86
|
+
|
|
87
|
+
constructor(id: string, name: string) {
|
|
88
|
+
this.id = id
|
|
89
|
+
this.group = new THREE.Group()
|
|
90
|
+
this.group.userData = { type: 'po', id }
|
|
91
|
+
|
|
92
|
+
// Hexagonal geometry
|
|
93
|
+
const geometry = new THREE.CircleGeometry(NODE.poRadius, NODE.poSides)
|
|
94
|
+
|
|
95
|
+
// Shader material
|
|
96
|
+
this.material = new THREE.ShaderMaterial({
|
|
97
|
+
vertexShader,
|
|
98
|
+
fragmentShader,
|
|
99
|
+
uniforms: {
|
|
100
|
+
uColor: { value: new THREE.Color(COLORS.nodeFill) },
|
|
101
|
+
uGlowColor: { value: new THREE.Color(COLORS.nodeGlow) },
|
|
102
|
+
uGlowIntensity: { value: 0.5 },
|
|
103
|
+
uTime: { value: 0 },
|
|
104
|
+
uHovered: { value: 0 },
|
|
105
|
+
uSelected: { value: 0 },
|
|
106
|
+
},
|
|
107
|
+
transparent: true,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
this.mesh = new THREE.Mesh(geometry, this.material)
|
|
111
|
+
this.mesh.userData = { type: 'po', id }
|
|
112
|
+
this.group.add(this.mesh)
|
|
113
|
+
|
|
114
|
+
// Status ring (hex outline)
|
|
115
|
+
const ringGeometry = new THREE.BufferGeometry()
|
|
116
|
+
const ringPoints: THREE.Vector3[] = []
|
|
117
|
+
for (let i = 0; i <= NODE.poSides; i++) {
|
|
118
|
+
const angle = (i / NODE.poSides) * Math.PI * 2 - Math.PI / 2
|
|
119
|
+
ringPoints.push(
|
|
120
|
+
new THREE.Vector3(
|
|
121
|
+
Math.cos(angle) * (NODE.poRadius + 3),
|
|
122
|
+
Math.sin(angle) * (NODE.poRadius + 3),
|
|
123
|
+
0
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
ringGeometry.setFromPoints(ringPoints)
|
|
128
|
+
|
|
129
|
+
this.statusRingMaterial = new THREE.LineBasicMaterial({
|
|
130
|
+
color: STATUS_COLORS.idle,
|
|
131
|
+
transparent: true,
|
|
132
|
+
opacity: 0.6,
|
|
133
|
+
})
|
|
134
|
+
this.statusRing = new THREE.LineLoop(ringGeometry, this.statusRingMaterial)
|
|
135
|
+
this.group.add(this.statusRing)
|
|
136
|
+
|
|
137
|
+
// CSS2D Label (name + status)
|
|
138
|
+
this.labelEl = document.createElement('div')
|
|
139
|
+
this.labelEl.className = 'canvas-node-label'
|
|
140
|
+
|
|
141
|
+
this.nameEl = document.createElement('span')
|
|
142
|
+
this.nameEl.className = 'canvas-node-name'
|
|
143
|
+
this.nameEl.textContent = name
|
|
144
|
+
|
|
145
|
+
this.statusEl = document.createElement('span')
|
|
146
|
+
this.statusEl.className = 'canvas-node-status'
|
|
147
|
+
this.statusEl.textContent = 'idle'
|
|
148
|
+
|
|
149
|
+
this.labelEl.appendChild(this.nameEl)
|
|
150
|
+
this.labelEl.appendChild(this.statusEl)
|
|
151
|
+
|
|
152
|
+
this.label = new CSS2DObject(this.labelEl)
|
|
153
|
+
this.label.position.set(0, -NODE.labelOffsetY, 0)
|
|
154
|
+
this.group.add(this.label)
|
|
155
|
+
|
|
156
|
+
// Notification badge
|
|
157
|
+
this.badgeEl = document.createElement('div')
|
|
158
|
+
this.badgeEl.className = 'canvas-node-badge'
|
|
159
|
+
this.badgeEl.style.display = 'none'
|
|
160
|
+
|
|
161
|
+
this.badgeCountEl = document.createElement('span')
|
|
162
|
+
this.badgeCountEl.textContent = '0'
|
|
163
|
+
this.badgeEl.appendChild(this.badgeCountEl)
|
|
164
|
+
|
|
165
|
+
this.badge = new CSS2DObject(this.badgeEl)
|
|
166
|
+
this.badge.position.set(NODE.badgeOffsetX, -NODE.badgeOffsetY, 0)
|
|
167
|
+
this.group.add(this.badge)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
setStatus(status: POStatus): void {
|
|
171
|
+
this.statusRingMaterial.color.setHex(STATUS_COLORS[status])
|
|
172
|
+
|
|
173
|
+
// Update glow intensity based on status
|
|
174
|
+
const intensity = status === 'idle' ? 0.3 : status === 'thinking' ? 0.8 : 0.6
|
|
175
|
+
this.material.uniforms.uGlowIntensity.value = intensity
|
|
176
|
+
|
|
177
|
+
// Update glow color for calling_tool
|
|
178
|
+
if (status === 'calling_tool') {
|
|
179
|
+
this.material.uniforms.uGlowColor.value.setHex(COLORS.statusCallingTool)
|
|
180
|
+
} else {
|
|
181
|
+
this.material.uniforms.uGlowColor.value.setHex(COLORS.nodeGlow)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.statusEl.textContent = status.replace('_', ' ')
|
|
185
|
+
this.statusEl.style.color = STATUS_CSS_COLORS[status]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
setDelegatedBy(callerName: string): void {
|
|
189
|
+
// Override visual to show this PO is working on behalf of another PO.
|
|
190
|
+
// The server doesn't send status updates for delegated POs, so we
|
|
191
|
+
// infer this from the caller's tool_calls.
|
|
192
|
+
this.statusRingMaterial.color.setHex(DELEGATED_COLOR)
|
|
193
|
+
this.statusRingMaterial.opacity = 1.0
|
|
194
|
+
this.material.uniforms.uGlowColor.value.setHex(DELEGATED_COLOR)
|
|
195
|
+
this.material.uniforms.uGlowIntensity.value = 0.7
|
|
196
|
+
this.statusEl.textContent = `called by ${callerName}`
|
|
197
|
+
this.statusEl.style.color = DELEGATED_CSS_COLOR
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
clearDelegated(): void {
|
|
201
|
+
// Restore to whatever the server status is — caller should
|
|
202
|
+
// call setStatus() after this to re-apply server state.
|
|
203
|
+
this.statusRingMaterial.opacity = 0.6
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setNotificationCount(count: number): void {
|
|
207
|
+
if (count > 0) {
|
|
208
|
+
this.badgeEl.style.display = 'flex'
|
|
209
|
+
this.badgeCountEl.textContent = String(count)
|
|
210
|
+
} else {
|
|
211
|
+
this.badgeEl.style.display = 'none'
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
setHovered(hovered: boolean): void {
|
|
216
|
+
this.material.uniforms.uHovered.value = hovered ? 1 : 0
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
setSelected(selected: boolean): void {
|
|
220
|
+
this.material.uniforms.uSelected.value = selected ? 1 : 0
|
|
221
|
+
this.statusRingMaterial.opacity = selected ? 1.0 : 0.6
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
setPosition(x: number, y: number): void {
|
|
225
|
+
this.targetPosition.set(x, y, 0)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
getPosition(): THREE.Vector3 {
|
|
229
|
+
return this.group.position.clone()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
update(_delta: number, elapsed: number): void {
|
|
233
|
+
// Lerp toward target position
|
|
234
|
+
this.group.position.lerp(this.targetPosition, ANIMATION.positionLerpFactor)
|
|
235
|
+
|
|
236
|
+
// Update time uniform for pulse animation
|
|
237
|
+
this.material.uniforms.uTime.value = elapsed
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
dispose(): void {
|
|
241
|
+
this.mesh.geometry.dispose()
|
|
242
|
+
this.material.dispose()
|
|
243
|
+
this.statusRing.geometry.dispose()
|
|
244
|
+
this.statusRingMaterial.dispose()
|
|
245
|
+
this.labelEl.remove()
|
|
246
|
+
this.badgeEl.remove()
|
|
247
|
+
this.group.parent?.remove(this.group)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js'
|
|
3
|
+
import { COLORS, NODE, ANIMATION } from '../constants'
|
|
4
|
+
|
|
5
|
+
type Phase = 'fadein' | 'active' | 'fadeout' | 'expired'
|
|
6
|
+
|
|
7
|
+
export class ToolCallNode {
|
|
8
|
+
readonly id: string
|
|
9
|
+
readonly callerPO: string
|
|
10
|
+
readonly group: THREE.Group
|
|
11
|
+
readonly mesh: THREE.Mesh
|
|
12
|
+
|
|
13
|
+
private material: THREE.MeshBasicMaterial
|
|
14
|
+
private label: CSS2DObject
|
|
15
|
+
private labelEl: HTMLDivElement
|
|
16
|
+
private phase: Phase = 'fadein'
|
|
17
|
+
private phaseTime = 0
|
|
18
|
+
private targetPosition = new THREE.Vector3()
|
|
19
|
+
|
|
20
|
+
constructor(id: string, toolName: string, callerPO: string) {
|
|
21
|
+
this.id = id
|
|
22
|
+
this.callerPO = callerPO
|
|
23
|
+
this.group = new THREE.Group()
|
|
24
|
+
this.group.userData = { type: 'toolcall', id }
|
|
25
|
+
|
|
26
|
+
// Diamond shape (rotated square)
|
|
27
|
+
const r = NODE.toolCallRadius
|
|
28
|
+
const shape = new THREE.Shape()
|
|
29
|
+
shape.moveTo(0, r)
|
|
30
|
+
shape.lineTo(r, 0)
|
|
31
|
+
shape.lineTo(0, -r)
|
|
32
|
+
shape.lineTo(-r, 0)
|
|
33
|
+
shape.closePath()
|
|
34
|
+
|
|
35
|
+
const geometry = new THREE.ShapeGeometry(shape)
|
|
36
|
+
|
|
37
|
+
this.material = new THREE.MeshBasicMaterial({
|
|
38
|
+
color: COLORS.toolCallFill,
|
|
39
|
+
transparent: true,
|
|
40
|
+
opacity: 0,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
this.mesh = new THREE.Mesh(geometry, this.material)
|
|
44
|
+
this.mesh.userData = { type: 'toolcall', id }
|
|
45
|
+
this.group.add(this.mesh)
|
|
46
|
+
|
|
47
|
+
// Label
|
|
48
|
+
this.labelEl = document.createElement('div')
|
|
49
|
+
this.labelEl.className = 'canvas-toolcall-label'
|
|
50
|
+
this.labelEl.textContent = toolName
|
|
51
|
+
|
|
52
|
+
this.label = new CSS2DObject(this.labelEl)
|
|
53
|
+
this.label.position.set(0, -(NODE.toolCallRadius + 12), 0)
|
|
54
|
+
this.group.add(this.label)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setPosition(x: number, y: number): void {
|
|
58
|
+
this.targetPosition.set(x, y, 0)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
triggerFadeOut(): void {
|
|
62
|
+
if (this.phase !== 'expired') {
|
|
63
|
+
this.phase = 'fadeout'
|
|
64
|
+
this.phaseTime = 0
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
isExpired(): boolean {
|
|
69
|
+
return this.phase === 'expired'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
update(delta: number): void {
|
|
73
|
+
this.phaseTime += delta
|
|
74
|
+
this.group.position.lerp(this.targetPosition, ANIMATION.positionLerpFactor)
|
|
75
|
+
|
|
76
|
+
switch (this.phase) {
|
|
77
|
+
case 'fadein':
|
|
78
|
+
this.material.opacity = Math.min(this.phaseTime / ANIMATION.toolCallFadeInDuration, 1)
|
|
79
|
+
if (this.phaseTime >= ANIMATION.toolCallFadeInDuration) {
|
|
80
|
+
this.phase = 'active'
|
|
81
|
+
this.phaseTime = 0
|
|
82
|
+
}
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
case 'active':
|
|
86
|
+
this.material.opacity = 1
|
|
87
|
+
if (this.phaseTime >= ANIMATION.toolCallActiveDuration) {
|
|
88
|
+
this.phase = 'fadeout'
|
|
89
|
+
this.phaseTime = 0
|
|
90
|
+
}
|
|
91
|
+
break
|
|
92
|
+
|
|
93
|
+
case 'fadeout':
|
|
94
|
+
this.material.opacity = Math.max(
|
|
95
|
+
1 - this.phaseTime / ANIMATION.toolCallFadeOutDuration,
|
|
96
|
+
0
|
|
97
|
+
)
|
|
98
|
+
this.labelEl.style.opacity = String(this.material.opacity)
|
|
99
|
+
if (this.phaseTime >= ANIMATION.toolCallFadeOutDuration) {
|
|
100
|
+
this.phase = 'expired'
|
|
101
|
+
}
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
case 'expired':
|
|
105
|
+
this.material.opacity = 0
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
dispose(): void {
|
|
111
|
+
this.mesh.geometry.dispose()
|
|
112
|
+
this.material.dispose()
|
|
113
|
+
this.labelEl.remove()
|
|
114
|
+
this.group.parent?.remove(this.group)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Canvas-specific type definitions
|
|
2
|
+
|
|
3
|
+
export interface CanvasNodeSelection {
|
|
4
|
+
type: 'po' | 'toolcall'
|
|
5
|
+
id: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ActiveToolCall {
|
|
9
|
+
id: string
|
|
10
|
+
toolName: string
|
|
11
|
+
callerPO: string
|
|
12
|
+
params: Record<string, unknown>
|
|
13
|
+
status: 'active' | 'completed' | 'error'
|
|
14
|
+
result?: string
|
|
15
|
+
startedAt: number
|
|
16
|
+
completedAt?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ActiveMessageArc {
|
|
20
|
+
id: string
|
|
21
|
+
from: string
|
|
22
|
+
to: string
|
|
23
|
+
timestamp: number
|
|
24
|
+
}
|
|
@@ -35,7 +35,6 @@ export function ContextMenu({ x, y, items, onClose }: ContextMenuProps) {
|
|
|
35
35
|
}
|
|
36
36
|
}, [onClose])
|
|
37
37
|
|
|
38
|
-
// Adjust position to stay within viewport
|
|
39
38
|
const adjustedStyle = {
|
|
40
39
|
top: y,
|
|
41
40
|
left: x,
|
|
@@ -44,7 +43,7 @@ export function ContextMenu({ x, y, items, onClose }: ContextMenuProps) {
|
|
|
44
43
|
return (
|
|
45
44
|
<div
|
|
46
45
|
ref={menuRef}
|
|
47
|
-
className="fixed z-50 bg-po-surface border border-po-border rounded
|
|
46
|
+
className="fixed z-50 bg-po-surface-2 border border-po-border rounded shadow-xl py-0.5 min-w-[140px]"
|
|
48
47
|
style={adjustedStyle}
|
|
49
48
|
>
|
|
50
49
|
{items.map((item, idx) => (
|
|
@@ -54,8 +53,10 @@ export function ContextMenu({ x, y, items, onClose }: ContextMenuProps) {
|
|
|
54
53
|
item.onClick()
|
|
55
54
|
onClose()
|
|
56
55
|
}}
|
|
57
|
-
className={`w-full text-left px-
|
|
58
|
-
item.danger
|
|
56
|
+
className={`w-full text-left px-2.5 py-1.5 text-xs transition-colors duration-150 flex items-center gap-1.5 ${
|
|
57
|
+
item.danger
|
|
58
|
+
? 'text-po-error hover:bg-po-surface-3'
|
|
59
|
+
: 'text-po-text-secondary hover:bg-po-surface-3 hover:text-po-text-primary'
|
|
59
60
|
}`}
|
|
60
61
|
>
|
|
61
62
|
{item.icon && <span>{item.icon}</span>}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react'
|
|
2
|
+
import { useStore, usePONotifications } from '../store'
|
|
3
|
+
import { useResize } from '../hooks/useResize'
|
|
4
|
+
import { MethodList } from './MethodList'
|
|
5
|
+
import { SourcePane } from './SourcePane'
|
|
6
|
+
import { Workspace } from './Workspace'
|
|
7
|
+
import { ContextMenu } from './ContextMenu'
|
|
8
|
+
import { PaneSlot } from './PaneSlot'
|
|
9
|
+
import type { PromptObject, CapabilityInfo } from '../types'
|
|
10
|
+
|
|
11
|
+
interface InspectorProps {
|
|
12
|
+
po: PromptObject
|
|
13
|
+
sendMessage: (target: string, content: string, newThread?: boolean) => void
|
|
14
|
+
createSession?: (target: string, name?: string) => void
|
|
15
|
+
switchSession: (target: string, sessionId: string) => void
|
|
16
|
+
createThread: (target: string) => void
|
|
17
|
+
updatePrompt: (target: string, prompt: string) => void
|
|
18
|
+
requestUsage?: (sessionId: string, includeTree?: boolean) => void
|
|
19
|
+
exportThread?: (sessionId: string, format?: string) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Inspector({
|
|
23
|
+
po,
|
|
24
|
+
sendMessage,
|
|
25
|
+
switchSession,
|
|
26
|
+
createThread,
|
|
27
|
+
updatePrompt,
|
|
28
|
+
requestUsage,
|
|
29
|
+
exportThread,
|
|
30
|
+
}: InspectorProps) {
|
|
31
|
+
const [selectedCapability, setSelectedCapability] = useState<CapabilityInfo | null>(null)
|
|
32
|
+
const [threadMenuOpen, setThreadMenuOpen] = useState(false)
|
|
33
|
+
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; sessionId: string } | null>(null)
|
|
34
|
+
const notifications = usePONotifications(po.name)
|
|
35
|
+
const topPaneCollapsed = useStore((s) => s.topPaneCollapsed)
|
|
36
|
+
const toggleTopPane = useStore((s) => s.toggleTopPane)
|
|
37
|
+
|
|
38
|
+
const topPaneResize = useResize({
|
|
39
|
+
direction: 'vertical',
|
|
40
|
+
initialSize: 260,
|
|
41
|
+
minSize: 120,
|
|
42
|
+
maxSize: 600,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const methodListResize = useResize({
|
|
46
|
+
direction: 'horizontal',
|
|
47
|
+
initialSize: 192,
|
|
48
|
+
minSize: 120,
|
|
49
|
+
maxSize: 320,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const sessions = po.sessions || []
|
|
53
|
+
const currentSessionId = po.current_session?.id
|
|
54
|
+
|
|
55
|
+
// Sort sessions: current first, then by updated_at desc
|
|
56
|
+
const sortedSessions = useMemo(() => {
|
|
57
|
+
return [...sessions].sort((a, b) => {
|
|
58
|
+
if (a.id === currentSessionId) return -1
|
|
59
|
+
if (b.id === currentSessionId) return 1
|
|
60
|
+
return (b.updated_at || '').localeCompare(a.updated_at || '')
|
|
61
|
+
})
|
|
62
|
+
}, [sessions, currentSessionId])
|
|
63
|
+
|
|
64
|
+
const isActive = po.status !== 'idle'
|
|
65
|
+
|
|
66
|
+
const statusDot = {
|
|
67
|
+
idle: 'bg-po-status-idle',
|
|
68
|
+
thinking: 'bg-po-status-active',
|
|
69
|
+
calling_tool: 'bg-po-status-calling',
|
|
70
|
+
}[po.status] || 'bg-po-status-idle'
|
|
71
|
+
|
|
72
|
+
const statusGlow = {
|
|
73
|
+
idle: '',
|
|
74
|
+
thinking: 'shadow-[0_0_6px_rgba(212,149,42,0.7)]',
|
|
75
|
+
calling_tool: 'shadow-[0_0_6px_rgba(59,154,110,0.7)]',
|
|
76
|
+
}[po.status] || ''
|
|
77
|
+
|
|
78
|
+
const statusLabelColor = {
|
|
79
|
+
idle: 'text-po-text-ghost',
|
|
80
|
+
thinking: 'text-po-status-active',
|
|
81
|
+
calling_tool: 'text-po-status-calling',
|
|
82
|
+
}[po.status] || 'text-po-text-ghost'
|
|
83
|
+
|
|
84
|
+
const statusLabel = {
|
|
85
|
+
idle: 'idle',
|
|
86
|
+
thinking: 'thinking...',
|
|
87
|
+
calling_tool: 'calling tool...',
|
|
88
|
+
}[po.status] || po.status
|
|
89
|
+
|
|
90
|
+
const handleThreadContextMenu = (e: React.MouseEvent, sessionId: string) => {
|
|
91
|
+
e.preventDefault()
|
|
92
|
+
setContextMenu({ x: e.clientX, y: e.clientY, sessionId })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="h-full flex flex-col">
|
|
97
|
+
{/* Inspector Header */}
|
|
98
|
+
<div className="h-8 bg-po-surface-2 border-b border-po-border flex items-center px-3 gap-2 flex-shrink-0">
|
|
99
|
+
<div className="relative flex-shrink-0">
|
|
100
|
+
<div className={`w-2 h-2 rounded-full ${statusDot} ${statusGlow} ${isActive ? 'animate-pulse' : ''}`} />
|
|
101
|
+
</div>
|
|
102
|
+
<span className="font-mono text-xs text-po-text-primary font-medium">{po.name}</span>
|
|
103
|
+
<span className={`text-2xs font-medium truncate ${statusLabelColor} ${isActive ? 'animate-pulse' : ''}`}>{statusLabel}</span>
|
|
104
|
+
{po.description && (
|
|
105
|
+
<span className="text-2xs text-po-text-ghost truncate hidden sm:inline">{po.description}</span>
|
|
106
|
+
)}
|
|
107
|
+
{notifications.length > 0 && (
|
|
108
|
+
<span className="text-2xs font-mono bg-po-warning text-po-bg px-1 rounded font-bold">
|
|
109
|
+
{notifications.length}
|
|
110
|
+
</span>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
<div className="flex-1" />
|
|
114
|
+
|
|
115
|
+
{/* Thread picker */}
|
|
116
|
+
<div className="relative">
|
|
117
|
+
<button
|
|
118
|
+
onClick={() => setThreadMenuOpen(!threadMenuOpen)}
|
|
119
|
+
className="flex items-center gap-1 text-2xs text-po-text-secondary hover:text-po-text-primary transition-colors duration-150"
|
|
120
|
+
>
|
|
121
|
+
<span className="font-mono">
|
|
122
|
+
{currentSessionId
|
|
123
|
+
? sessions.find(s => s.id === currentSessionId)?.name || `Thread ${currentSessionId.slice(0, 6)}`
|
|
124
|
+
: 'No thread'}
|
|
125
|
+
</span>
|
|
126
|
+
<svg className={`w-3 h-3 transition-transform ${threadMenuOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
127
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
128
|
+
</svg>
|
|
129
|
+
</button>
|
|
130
|
+
|
|
131
|
+
{threadMenuOpen && (
|
|
132
|
+
<div className="absolute right-0 top-full mt-1 w-56 bg-po-surface-2 border border-po-border rounded shadow-xl z-50 overflow-hidden">
|
|
133
|
+
<button
|
|
134
|
+
onClick={() => { createThread(po.name); setThreadMenuOpen(false) }}
|
|
135
|
+
className="w-full text-left px-2.5 py-1.5 text-xs text-po-accent hover:bg-po-surface-3 transition-colors duration-150 border-b border-po-border"
|
|
136
|
+
>
|
|
137
|
+
+ New Thread
|
|
138
|
+
</button>
|
|
139
|
+
<div className="max-h-48 overflow-auto">
|
|
140
|
+
{sortedSessions.map((session) => (
|
|
141
|
+
<button
|
|
142
|
+
key={session.id}
|
|
143
|
+
onClick={() => { switchSession(po.name, session.id); setThreadMenuOpen(false) }}
|
|
144
|
+
onContextMenu={(e) => handleThreadContextMenu(e, session.id)}
|
|
145
|
+
className={`w-full text-left px-2.5 py-1.5 text-xs transition-colors duration-150 ${
|
|
146
|
+
session.id === currentSessionId
|
|
147
|
+
? 'bg-po-accent-wash text-po-accent'
|
|
148
|
+
: 'text-po-text-secondary hover:bg-po-surface-3'
|
|
149
|
+
}`}
|
|
150
|
+
>
|
|
151
|
+
<div className="flex items-center gap-1.5">
|
|
152
|
+
{session.thread_type === 'delegation' && <span className="text-po-status-delegated">↳</span>}
|
|
153
|
+
<span className="font-mono truncate flex-1">
|
|
154
|
+
{session.name || `Thread ${session.id.slice(0, 6)}`}
|
|
155
|
+
</span>
|
|
156
|
+
<span className="text-2xs text-po-text-ghost">{session.message_count}m</span>
|
|
157
|
+
</div>
|
|
158
|
+
{session.parent_po && (
|
|
159
|
+
<div className="text-2xs text-po-status-delegated mt-0.5">from {session.parent_po}</div>
|
|
160
|
+
)}
|
|
161
|
+
</button>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{/* Top: Methods + Source (collapsible, resizable height) */}
|
|
170
|
+
<PaneSlot
|
|
171
|
+
label="Methods | Source"
|
|
172
|
+
collapsed={topPaneCollapsed}
|
|
173
|
+
onToggle={toggleTopPane}
|
|
174
|
+
height={topPaneResize.size}
|
|
175
|
+
resizeHandle={
|
|
176
|
+
<div
|
|
177
|
+
className="resize-handle-h"
|
|
178
|
+
onMouseDown={topPaneResize.onMouseDown}
|
|
179
|
+
/>
|
|
180
|
+
}
|
|
181
|
+
>
|
|
182
|
+
<div className="flex h-full">
|
|
183
|
+
{/* Method List (resizable width) */}
|
|
184
|
+
<div style={{ width: methodListResize.size }} className="flex-shrink-0">
|
|
185
|
+
<MethodList
|
|
186
|
+
po={po}
|
|
187
|
+
selectedCapability={selectedCapability}
|
|
188
|
+
onSelectCapability={setSelectedCapability}
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Resize handle */}
|
|
193
|
+
<div
|
|
194
|
+
className="resize-handle"
|
|
195
|
+
onMouseDown={methodListResize.onMouseDown}
|
|
196
|
+
/>
|
|
197
|
+
|
|
198
|
+
{/* Source Pane */}
|
|
199
|
+
<SourcePane
|
|
200
|
+
po={po}
|
|
201
|
+
selectedCapability={selectedCapability}
|
|
202
|
+
onSave={(prompt) => updatePrompt(po.name, prompt)}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
</PaneSlot>
|
|
206
|
+
|
|
207
|
+
{/* Bottom: Workspace */}
|
|
208
|
+
<div className="flex-1 overflow-hidden">
|
|
209
|
+
<Workspace po={po} sendMessage={sendMessage} />
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{/* Context menu for thread right-click */}
|
|
213
|
+
{contextMenu && (
|
|
214
|
+
<ContextMenu
|
|
215
|
+
x={contextMenu.x}
|
|
216
|
+
y={contextMenu.y}
|
|
217
|
+
onClose={() => setContextMenu(null)}
|
|
218
|
+
items={[
|
|
219
|
+
...(requestUsage ? [
|
|
220
|
+
{ label: 'View Usage', onClick: () => requestUsage(contextMenu.sessionId) },
|
|
221
|
+
{ label: 'View Tree Usage', onClick: () => requestUsage(contextMenu.sessionId, true) },
|
|
222
|
+
] : []),
|
|
223
|
+
...(exportThread ? [
|
|
224
|
+
{ label: 'Export Markdown', onClick: () => exportThread(contextMenu.sessionId, 'markdown') },
|
|
225
|
+
{ label: 'Export JSON', onClick: () => exportThread(contextMenu.sessionId, 'json') },
|
|
226
|
+
] : []),
|
|
227
|
+
]}
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
)
|
|
232
|
+
}
|