prompt_objects 0.3.1 → 0.4.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 +15 -0
- data/Gemfile.lock +31 -29
- data/frontend/package-lock.json +123 -0
- data/frontend/package.json +4 -0
- data/frontend/src/App.tsx +58 -51
- 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/ChatPanel.tsx +13 -5
- data/frontend/src/components/Header.tsx +13 -1
- data/frontend/src/hooks/useWebSocket.ts +246 -189
- data/frontend/src/index.css +48 -0
- data/frontend/src/store/index.ts +19 -0
- data/frontend/src/types/index.ts +3 -0
- data/lib/prompt_objects/connectors/mcp.rb +3 -6
- data/lib/prompt_objects/environment.rb +8 -0
- data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +1 -2
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +1 -2
- data/lib/prompt_objects/primitives/list_files.rb +1 -2
- data/lib/prompt_objects/prompt_object.rb +24 -6
- data/lib/prompt_objects/server/app.rb +9 -0
- data/lib/prompt_objects/server/public/assets/index-6y64NXFy.css +1 -0
- data/lib/prompt_objects/server/public/assets/index-xvyeb-5Z.js +4345 -0
- data/lib/prompt_objects/server/public/index.html +2 -2
- data/prompt_objects.gemspec +1 -1
- metadata +17 -4
- 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,113 @@
|
|
|
1
|
+
import { useEffect, useRef, useCallback } from 'react'
|
|
2
|
+
import { useStore } from '../store'
|
|
3
|
+
import { useCanvasStore } from './canvasStore'
|
|
4
|
+
import { SceneManager } from './SceneManager'
|
|
5
|
+
import { InspectorPanel } from './inspector/InspectorPanel'
|
|
6
|
+
|
|
7
|
+
export function CanvasView() {
|
|
8
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
9
|
+
const sceneRef = useRef<SceneManager | null>(null)
|
|
10
|
+
const syncScheduled = useRef(false)
|
|
11
|
+
|
|
12
|
+
// Schedule a throttled sync via requestAnimationFrame
|
|
13
|
+
const scheduleSync = useCallback(() => {
|
|
14
|
+
if (syncScheduled.current) return
|
|
15
|
+
syncScheduled.current = true
|
|
16
|
+
requestAnimationFrame(() => {
|
|
17
|
+
syncScheduled.current = false
|
|
18
|
+
const scene = sceneRef.current
|
|
19
|
+
if (!scene) return
|
|
20
|
+
const state = useStore.getState()
|
|
21
|
+
scene.syncPromptObjects(state.promptObjects)
|
|
22
|
+
scene.syncBusMessages(state.busMessages)
|
|
23
|
+
scene.syncNotifications(state.notifications)
|
|
24
|
+
})
|
|
25
|
+
}, [])
|
|
26
|
+
|
|
27
|
+
// Mount/unmount SceneManager
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const container = containerRef.current
|
|
30
|
+
if (!container) return
|
|
31
|
+
|
|
32
|
+
const scene = new SceneManager(container)
|
|
33
|
+
sceneRef.current = scene
|
|
34
|
+
|
|
35
|
+
// Initial sync
|
|
36
|
+
const state = useStore.getState()
|
|
37
|
+
scene.syncPromptObjects(state.promptObjects)
|
|
38
|
+
scene.syncBusMessages(state.busMessages)
|
|
39
|
+
scene.syncNotifications(state.notifications)
|
|
40
|
+
scene.start()
|
|
41
|
+
|
|
42
|
+
// Fit all after a short delay to let force layout settle
|
|
43
|
+
const fitTimer = setTimeout(() => scene.fitAll(), 500)
|
|
44
|
+
|
|
45
|
+
return () => {
|
|
46
|
+
clearTimeout(fitTimer)
|
|
47
|
+
scene.dispose()
|
|
48
|
+
sceneRef.current = null
|
|
49
|
+
}
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
52
|
+
// Subscribe to store changes (non-React API to avoid re-renders)
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const unsub = useStore.subscribe(scheduleSync)
|
|
55
|
+
return unsub
|
|
56
|
+
}, [scheduleSync])
|
|
57
|
+
|
|
58
|
+
// Keyboard shortcuts
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const handler = (e: KeyboardEvent) => {
|
|
61
|
+
if (e.key === 'f' && !e.ctrlKey && !e.metaKey) {
|
|
62
|
+
// Don't intercept when typing in an input
|
|
63
|
+
if ((e.target as HTMLElement).tagName === 'INPUT' || (e.target as HTMLElement).tagName === 'TEXTAREA') return
|
|
64
|
+
sceneRef.current?.fitAll()
|
|
65
|
+
}
|
|
66
|
+
if (e.key === 'Escape') {
|
|
67
|
+
useCanvasStore.getState().selectNode(null)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
window.addEventListener('keydown', handler)
|
|
71
|
+
return () => window.removeEventListener('keydown', handler)
|
|
72
|
+
}, [])
|
|
73
|
+
|
|
74
|
+
const showLabels = useCanvasStore((s) => s.showLabels)
|
|
75
|
+
const toggleLabels = useCanvasStore((s) => s.toggleLabels)
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex-1 flex overflow-hidden relative">
|
|
79
|
+
{/* Three.js container */}
|
|
80
|
+
<div ref={containerRef} className="flex-1 relative" />
|
|
81
|
+
|
|
82
|
+
{/* Toolbar overlay */}
|
|
83
|
+
<div className="absolute top-3 left-3 flex gap-2 z-10">
|
|
84
|
+
<button
|
|
85
|
+
onClick={() => sceneRef.current?.fitAll()}
|
|
86
|
+
className="px-3 py-1.5 text-sm bg-po-surface/80 backdrop-blur border border-po-border rounded hover:border-po-accent transition-colors text-gray-300 hover:text-white"
|
|
87
|
+
title="Fit all nodes (F)"
|
|
88
|
+
>
|
|
89
|
+
Fit All
|
|
90
|
+
</button>
|
|
91
|
+
<button
|
|
92
|
+
onClick={toggleLabels}
|
|
93
|
+
className={`px-3 py-1.5 text-sm backdrop-blur border rounded transition-colors ${
|
|
94
|
+
showLabels
|
|
95
|
+
? 'bg-po-accent/20 border-po-accent text-white'
|
|
96
|
+
: 'bg-po-surface/80 border-po-border text-gray-300 hover:text-white'
|
|
97
|
+
}`}
|
|
98
|
+
title="Toggle labels"
|
|
99
|
+
>
|
|
100
|
+
Labels
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Help hint */}
|
|
105
|
+
<div className="absolute bottom-3 left-3 text-xs text-gray-500 z-10">
|
|
106
|
+
Scroll to zoom · Shift+drag to pan · F to fit · Click node to inspect
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Inspector panel */}
|
|
110
|
+
<InspectorPanel />
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forceSimulation,
|
|
3
|
+
forceManyBody,
|
|
4
|
+
forceCenter,
|
|
5
|
+
forceCollide,
|
|
6
|
+
forceLink,
|
|
7
|
+
type Simulation,
|
|
8
|
+
type SimulationNodeDatum,
|
|
9
|
+
type SimulationLinkDatum,
|
|
10
|
+
} from 'd3-force'
|
|
11
|
+
import { FORCE } from './constants'
|
|
12
|
+
|
|
13
|
+
export interface ForceNode extends SimulationNodeDatum {
|
|
14
|
+
id: string
|
|
15
|
+
type: 'po' | 'toolcall'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ForceLink extends SimulationLinkDatum<ForceNode> {
|
|
19
|
+
id: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ForceLayout {
|
|
23
|
+
private simulation: Simulation<ForceNode, ForceLink>
|
|
24
|
+
private nodes: ForceNode[] = []
|
|
25
|
+
private links: ForceLink[] = []
|
|
26
|
+
private dirty = false
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
this.simulation = forceSimulation<ForceNode, ForceLink>()
|
|
30
|
+
.alphaDecay(FORCE.alphaDecay)
|
|
31
|
+
.velocityDecay(FORCE.velocityDecay)
|
|
32
|
+
.force('charge', forceManyBody<ForceNode>().strength(FORCE.chargeStrength))
|
|
33
|
+
.force('center', forceCenter<ForceNode>(0, 0).strength(FORCE.centerStrength))
|
|
34
|
+
.force(
|
|
35
|
+
'collision',
|
|
36
|
+
forceCollide<ForceNode>().radius(FORCE.collisionRadius)
|
|
37
|
+
)
|
|
38
|
+
.force(
|
|
39
|
+
'link',
|
|
40
|
+
forceLink<ForceNode, ForceLink>()
|
|
41
|
+
.id((d) => d.id)
|
|
42
|
+
.distance(FORCE.linkDistance)
|
|
43
|
+
)
|
|
44
|
+
.stop() // Manual tick mode — we call tick() from animation loop
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
addNode(id: string, type: 'po' | 'toolcall'): void {
|
|
48
|
+
if (this.nodes.find((n) => n.id === id)) return
|
|
49
|
+
this.nodes.push({ id, type })
|
|
50
|
+
this.dirty = true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
removeNode(id: string): void {
|
|
54
|
+
const idx = this.nodes.findIndex((n) => n.id === id)
|
|
55
|
+
if (idx === -1) return
|
|
56
|
+
this.nodes.splice(idx, 1)
|
|
57
|
+
// Also remove any links referencing this node
|
|
58
|
+
this.links = this.links.filter((l) => {
|
|
59
|
+
const src = typeof l.source === 'object' ? (l.source as ForceNode).id : l.source
|
|
60
|
+
const tgt = typeof l.target === 'object' ? (l.target as ForceNode).id : l.target
|
|
61
|
+
return src !== id && tgt !== id
|
|
62
|
+
})
|
|
63
|
+
this.dirty = true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
addLink(id: string, sourceId: string, targetId: string): void {
|
|
67
|
+
if (this.links.find((l) => l.id === id)) return
|
|
68
|
+
this.links.push({ id, source: sourceId, target: targetId })
|
|
69
|
+
this.dirty = true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
removeLink(id: string): void {
|
|
73
|
+
const idx = this.links.findIndex((l) => l.id === id)
|
|
74
|
+
if (idx === -1) return
|
|
75
|
+
this.links.splice(idx, 1)
|
|
76
|
+
this.dirty = true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
tick(): void {
|
|
80
|
+
if (this.dirty) {
|
|
81
|
+
this.rebuild()
|
|
82
|
+
this.dirty = false
|
|
83
|
+
}
|
|
84
|
+
this.simulation.tick()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getPositions(): Map<string, { x: number; y: number }> {
|
|
88
|
+
const positions = new Map<string, { x: number; y: number }>()
|
|
89
|
+
for (const node of this.nodes) {
|
|
90
|
+
positions.set(node.id, { x: node.x ?? 0, y: node.y ?? 0 })
|
|
91
|
+
}
|
|
92
|
+
return positions
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
reheat(): void {
|
|
96
|
+
this.simulation.alpha(0.8).restart().stop()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private rebuild(): void {
|
|
100
|
+
this.simulation.nodes(this.nodes)
|
|
101
|
+
const linkForce = this.simulation.force('link') as ReturnType<
|
|
102
|
+
typeof forceLink<ForceNode, ForceLink>
|
|
103
|
+
>
|
|
104
|
+
if (linkForce) {
|
|
105
|
+
linkForce.links(this.links)
|
|
106
|
+
}
|
|
107
|
+
this.reheat()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
dispose(): void {
|
|
111
|
+
this.simulation.stop()
|
|
112
|
+
this.nodes = []
|
|
113
|
+
this.links = []
|
|
114
|
+
}
|
|
115
|
+
}
|