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,260 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react'
|
|
2
|
+
import { useStore } from '../store'
|
|
3
|
+
import { MarkdownMessage } from './MarkdownMessage'
|
|
4
|
+
import type { PromptObject, Message, ToolCall } from '../types'
|
|
5
|
+
|
|
6
|
+
interface WorkspaceProps {
|
|
7
|
+
po: PromptObject
|
|
8
|
+
sendMessage: (target: string, content: string, newThread?: boolean) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Workspace({ po, sendMessage }: WorkspaceProps) {
|
|
12
|
+
const [input, setInput] = useState('')
|
|
13
|
+
const [continueThread, setContinueThread] = useState(false)
|
|
14
|
+
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
15
|
+
const { streamingContent, connected } = useStore()
|
|
16
|
+
|
|
17
|
+
const messages = po.current_session?.messages || []
|
|
18
|
+
const streaming = streamingContent[po.name]
|
|
19
|
+
const hasMessages = messages.length > 0
|
|
20
|
+
const isBusy = po.status !== 'idle' && connected
|
|
21
|
+
const canSend = connected && !isBusy && !!input.trim()
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
25
|
+
}, [messages, streaming])
|
|
26
|
+
|
|
27
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
28
|
+
e.preventDefault()
|
|
29
|
+
if (!input.trim()) return
|
|
30
|
+
const content = input.trim()
|
|
31
|
+
const shouldCreateNewThread = !continueThread && hasMessages
|
|
32
|
+
sendMessage(po.name, content, shouldCreateNewThread)
|
|
33
|
+
setInput('')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
37
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
38
|
+
e.preventDefault()
|
|
39
|
+
if (canSend) {
|
|
40
|
+
handleSubmit(e)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="h-full flex flex-col">
|
|
47
|
+
{/* Messages */}
|
|
48
|
+
<div className="flex-1 overflow-auto px-4 py-2 space-y-1">
|
|
49
|
+
{messages.length === 0 && !streaming && (
|
|
50
|
+
<div className="h-full flex items-center justify-center">
|
|
51
|
+
<span className="font-mono text-xs text-po-text-ghost">> _</span>
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
|
|
55
|
+
{messages.map((message, index) => (
|
|
56
|
+
<WorkspaceEntry key={index} message={message} />
|
|
57
|
+
))}
|
|
58
|
+
|
|
59
|
+
{/* Streaming content */}
|
|
60
|
+
{streaming && (
|
|
61
|
+
<div className="py-1">
|
|
62
|
+
<MarkdownMessage content={streaming} className="text-po-text-primary text-xs" />
|
|
63
|
+
<span className="inline-block w-1.5 h-3.5 bg-po-accent animate-pulse ml-0.5 align-text-bottom" />
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
<div ref={messagesEndRef} />
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Input */}
|
|
71
|
+
<div className="border-t border-po-border bg-po-surface-2 px-4 py-2">
|
|
72
|
+
{!connected && (
|
|
73
|
+
<div className="mb-1.5 text-2xs text-po-warning flex items-center gap-1.5 font-mono">
|
|
74
|
+
<div className="w-1.5 h-1.5 rounded-full bg-po-warning animate-pulse" />
|
|
75
|
+
reconnecting...
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
<form onSubmit={handleSubmit} className="flex items-center gap-2">
|
|
80
|
+
<span className="text-po-text-ghost font-mono text-xs select-none">></span>
|
|
81
|
+
<input
|
|
82
|
+
type="text"
|
|
83
|
+
value={input}
|
|
84
|
+
onChange={(e) => setInput(e.target.value)}
|
|
85
|
+
onKeyDown={handleKeyDown}
|
|
86
|
+
placeholder={!connected ? 'disconnected' : isBusy ? `${po.status.replace('_', ' ')}...` : ''}
|
|
87
|
+
className="flex-1 bg-transparent text-po-text-primary font-mono text-xs placeholder-po-text-ghost focus:outline-none disabled:opacity-50"
|
|
88
|
+
disabled={isBusy}
|
|
89
|
+
/>
|
|
90
|
+
</form>
|
|
91
|
+
|
|
92
|
+
{/* Thread toggle */}
|
|
93
|
+
{hasMessages && (
|
|
94
|
+
<div className="flex items-center gap-2 mt-1.5 text-2xs font-mono">
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={() => setContinueThread(false)}
|
|
98
|
+
className={`px-1.5 py-0.5 rounded transition-colors duration-150 ${
|
|
99
|
+
!continueThread
|
|
100
|
+
? 'bg-po-accent-wash text-po-accent'
|
|
101
|
+
: 'text-po-text-ghost hover:text-po-text-secondary'
|
|
102
|
+
}`}
|
|
103
|
+
>
|
|
104
|
+
new thread
|
|
105
|
+
</button>
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
onClick={() => setContinueThread(true)}
|
|
109
|
+
className={`px-1.5 py-0.5 rounded transition-colors duration-150 ${
|
|
110
|
+
continueThread
|
|
111
|
+
? 'bg-po-accent-wash text-po-accent'
|
|
112
|
+
: 'text-po-text-ghost hover:text-po-text-secondary'
|
|
113
|
+
}`}
|
|
114
|
+
>
|
|
115
|
+
continue
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function WorkspaceEntry({ message }: { message: Message }) {
|
|
125
|
+
const isUser = message.role === 'user'
|
|
126
|
+
const isAssistant = message.role === 'assistant'
|
|
127
|
+
const isTool = message.role === 'tool'
|
|
128
|
+
|
|
129
|
+
// Tool results
|
|
130
|
+
if (isTool) {
|
|
131
|
+
const results = message.results || []
|
|
132
|
+
if (results.length === 0) return null
|
|
133
|
+
return (
|
|
134
|
+
<div className="space-y-1 pl-4">
|
|
135
|
+
{results.map((result, idx) => (
|
|
136
|
+
<ToolResultFrame key={result.tool_call_id || idx} result={result} />
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// User message: REPL-style "> message"
|
|
143
|
+
if (isUser) {
|
|
144
|
+
return (
|
|
145
|
+
<div className="py-1">
|
|
146
|
+
<span className="font-mono text-xs text-po-text-ghost select-none">> </span>
|
|
147
|
+
<span className="font-mono text-xs text-po-text-primary whitespace-pre-wrap">{message.content}</span>
|
|
148
|
+
</div>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Assistant message: plain text with markdown
|
|
153
|
+
if (isAssistant) {
|
|
154
|
+
return (
|
|
155
|
+
<div className="py-1">
|
|
156
|
+
{message.content && (
|
|
157
|
+
<MarkdownMessage content={message.content} className="text-po-text-primary text-xs" />
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{/* Tool calls as bordered frames */}
|
|
161
|
+
{message.tool_calls && message.tool_calls.length > 0 && (
|
|
162
|
+
<div className="mt-1 space-y-1">
|
|
163
|
+
{message.tool_calls.map((tc) => (
|
|
164
|
+
<ToolCallFrame key={tc.id} toolCall={tc} />
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function ToolCallFrame({ toolCall }: { toolCall: ToolCall }) {
|
|
176
|
+
const [expanded, setExpanded] = useState(false)
|
|
177
|
+
const { notifications } = useStore()
|
|
178
|
+
|
|
179
|
+
// ask_human: amber-bordered frame
|
|
180
|
+
if (toolCall.name === 'ask_human') {
|
|
181
|
+
const question = toolCall.arguments.question as string
|
|
182
|
+
const options = toolCall.arguments.options as string[] | undefined
|
|
183
|
+
const isPending = notifications.some((n) => n.message === question)
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className="border-l-2 border-po-warning bg-po-accent-wash rounded-r px-3 py-2 my-1">
|
|
187
|
+
<div className="flex items-center gap-2 mb-1">
|
|
188
|
+
<span className="text-2xs font-mono text-po-warning">ask_human</span>
|
|
189
|
+
{isPending ? (
|
|
190
|
+
<span className="text-2xs bg-po-warning text-po-bg px-1.5 py-0.5 rounded font-bold animate-pulse">
|
|
191
|
+
PENDING
|
|
192
|
+
</span>
|
|
193
|
+
) : (
|
|
194
|
+
<span className="text-2xs bg-po-success text-po-bg px-1.5 py-0.5 rounded font-bold">
|
|
195
|
+
RESOLVED
|
|
196
|
+
</span>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
<p className="text-xs text-po-text-primary mb-1.5">{question}</p>
|
|
200
|
+
{options && options.length > 0 && (
|
|
201
|
+
<div className="flex flex-wrap gap-1.5">
|
|
202
|
+
{options.map((opt, i) => (
|
|
203
|
+
<span key={i} className="text-2xs font-mono bg-po-surface-2 px-1.5 py-0.5 rounded text-po-text-secondary">
|
|
204
|
+
{opt}
|
|
205
|
+
</span>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Default tool call: bordered frame
|
|
214
|
+
const argsStr = JSON.stringify(toolCall.arguments, null, 2)
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className="border-l-2 border-po-status-calling rounded-r overflow-hidden my-1">
|
|
218
|
+
<button
|
|
219
|
+
onClick={() => setExpanded(!expanded)}
|
|
220
|
+
className="w-full text-left px-3 py-1 flex items-center gap-1.5 hover:bg-po-surface-2 transition-colors duration-150 bg-po-surface"
|
|
221
|
+
>
|
|
222
|
+
<span className="text-2xs text-po-text-ghost">{expanded ? '\u25BC' : '\u25B8'}</span>
|
|
223
|
+
<span className="text-xs font-mono text-po-status-calling">{toolCall.name}</span>
|
|
224
|
+
<span className="text-2xs text-po-text-ghost">
|
|
225
|
+
({Object.keys(toolCall.arguments).length} args)
|
|
226
|
+
</span>
|
|
227
|
+
</button>
|
|
228
|
+
{expanded && (
|
|
229
|
+
<pre className="px-3 py-1.5 bg-po-surface text-2xs text-po-text-tertiary font-mono whitespace-pre-wrap break-all">
|
|
230
|
+
{argsStr}
|
|
231
|
+
</pre>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function ToolResultFrame({ result }: { result: { tool_call_id: string; name?: string; content: string } }) {
|
|
238
|
+
const [expanded, setExpanded] = useState(false)
|
|
239
|
+
const content = result.content || ''
|
|
240
|
+
const chars = content.length
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div className="border-l-2 border-po-status-calling rounded-r overflow-hidden my-0.5">
|
|
244
|
+
<button
|
|
245
|
+
onClick={() => setExpanded(!expanded)}
|
|
246
|
+
className="w-full text-left px-3 py-1 flex items-center gap-1.5 hover:bg-po-surface-2 transition-colors duration-150 bg-po-surface"
|
|
247
|
+
>
|
|
248
|
+
<span className="text-2xs text-po-text-ghost">{expanded ? '\u25BC' : '\u25B8'}</span>
|
|
249
|
+
<span className="text-2xs text-po-text-tertiary">Result</span>
|
|
250
|
+
{result.name && <span className="text-2xs text-po-status-calling font-mono">{result.name}</span>}
|
|
251
|
+
<span className="text-2xs text-po-text-ghost">({chars} chars)</span>
|
|
252
|
+
</button>
|
|
253
|
+
{expanded && (
|
|
254
|
+
<pre className="px-3 py-1.5 bg-po-surface text-2xs text-po-text-tertiary font-mono whitespace-pre-wrap break-all">
|
|
255
|
+
{content}
|
|
256
|
+
</pre>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export { MessageBus } from './MessageBus'
|
|
1
|
+
export { SystemBar } from './SystemBar'
|
|
2
|
+
export { ObjectList } from './ObjectList'
|
|
3
|
+
export { Inspector } from './Inspector'
|
|
4
|
+
export { MethodList } from './MethodList'
|
|
5
|
+
export { SourcePane } from './SourcePane'
|
|
6
|
+
export { Workspace } from './Workspace'
|
|
7
|
+
export { Transcript } from './Transcript'
|
|
9
8
|
export { NotificationPanel } from './NotificationPanel'
|
|
10
9
|
export { MarkdownMessage } from './MarkdownMessage'
|
|
11
|
-
export {
|
|
10
|
+
export { ModelSelector } from './ModelSelector'
|
|
11
|
+
export { ContextMenu } from './ContextMenu'
|
|
12
|
+
export { UsagePanel } from './UsagePanel'
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
interface UseResizeOptions {
|
|
4
|
+
direction: 'horizontal' | 'vertical'
|
|
5
|
+
initialSize: number
|
|
6
|
+
minSize: number
|
|
7
|
+
maxSize: number
|
|
8
|
+
/** If true, dragging down/right increases size. If false (for bottom panels), dragging up increases. */
|
|
9
|
+
inverted?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useResize({ direction, initialSize, minSize, maxSize, inverted = false }: UseResizeOptions) {
|
|
13
|
+
const [size, setSize] = useState(initialSize)
|
|
14
|
+
const dragging = useRef(false)
|
|
15
|
+
const startPos = useRef(0)
|
|
16
|
+
const startSize = useRef(0)
|
|
17
|
+
|
|
18
|
+
const onMouseDown = useCallback((e: React.MouseEvent) => {
|
|
19
|
+
e.preventDefault()
|
|
20
|
+
dragging.current = true
|
|
21
|
+
startPos.current = direction === 'horizontal' ? e.clientX : e.clientY
|
|
22
|
+
startSize.current = size
|
|
23
|
+
document.body.style.cursor = direction === 'horizontal' ? 'col-resize' : 'row-resize'
|
|
24
|
+
document.body.style.userSelect = 'none'
|
|
25
|
+
}, [direction, size])
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
29
|
+
if (!dragging.current) return
|
|
30
|
+
const currentPos = direction === 'horizontal' ? e.clientX : e.clientY
|
|
31
|
+
const delta = currentPos - startPos.current
|
|
32
|
+
const newSize = inverted
|
|
33
|
+
? startSize.current - delta
|
|
34
|
+
: startSize.current + delta
|
|
35
|
+
setSize(Math.min(maxSize, Math.max(minSize, newSize)))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const onMouseUp = () => {
|
|
39
|
+
if (dragging.current) {
|
|
40
|
+
dragging.current = false
|
|
41
|
+
document.body.style.cursor = ''
|
|
42
|
+
document.body.style.userSelect = ''
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
document.addEventListener('mousemove', onMouseMove)
|
|
47
|
+
document.addEventListener('mouseup', onMouseUp)
|
|
48
|
+
return () => {
|
|
49
|
+
document.removeEventListener('mousemove', onMouseMove)
|
|
50
|
+
document.removeEventListener('mouseup', onMouseUp)
|
|
51
|
+
}
|
|
52
|
+
}, [direction, minSize, maxSize, inverted])
|
|
53
|
+
|
|
54
|
+
return { size, onMouseDown }
|
|
55
|
+
}
|