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
@@ -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">&gt; _</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">&gt;</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">&gt; </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 { Header } from './Header'
2
- export { Dashboard } from './Dashboard'
3
- export { POCard } from './POCard'
4
- export { PODetail } from './PODetail'
5
- export { ChatPanel } from './ChatPanel'
6
- export { SessionsPanel } from './SessionsPanel'
7
- export { CapabilitiesPanel } from './CapabilitiesPanel'
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 { PromptPanel } from './PromptPanel'
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
+ }