prompt_objects 0.4.0 → 0.6.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 +33 -0
- data/CLAUDE.md +113 -44
- data/README.md +140 -14
- data/frontend/index.html +5 -1
- data/frontend/src/App.tsx +72 -79
- data/frontend/src/canvas/CanvasView.tsx +5 -5
- data/frontend/src/canvas/constants.ts +31 -31
- data/frontend/src/canvas/inspector/InspectorPanel.tsx +4 -4
- data/frontend/src/canvas/inspector/POInspector.tsx +35 -35
- data/frontend/src/canvas/inspector/ToolCallInspector.tsx +13 -13
- data/frontend/src/canvas/nodes/PONode.ts +2 -2
- data/frontend/src/components/ContextMenu.tsx +5 -4
- data/frontend/src/components/EnvDataPane.tsx +69 -0
- data/frontend/src/components/Inspector.tsx +263 -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 +70 -0
- data/frontend/src/index.css +27 -10
- data/frontend/src/store/index.ts +36 -0
- data/frontend/src/types/index.ts +13 -0
- data/frontend/tailwind.config.js +28 -9
- data/lib/prompt_objects/capability.rb +23 -1
- data/lib/prompt_objects/connectors/mcp.rb +2 -16
- data/lib/prompt_objects/environment.rb +15 -0
- data/lib/prompt_objects/llm/openai_adapter.rb +22 -0
- data/lib/prompt_objects/mcp/tools/inspect_po.rb +1 -31
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +1 -6
- data/lib/prompt_objects/prompt_object.rb +239 -7
- data/lib/prompt_objects/server/api/routes.rb +16 -48
- data/lib/prompt_objects/server/app.rb +14 -0
- data/lib/prompt_objects/server/public/assets/{index-xvyeb-5Z.js → index-DEPawnfZ.js} +206 -206
- data/lib/prompt_objects/server/public/assets/index-oMrRce1m.css +1 -0
- data/lib/prompt_objects/server/public/index.html +7 -3
- data/lib/prompt_objects/server/websocket_handler.rb +41 -98
- data/lib/prompt_objects/server.rb +6 -62
- data/lib/prompt_objects/session/store.rb +176 -4
- data/lib/prompt_objects/universal/delete_env_data.rb +70 -0
- data/lib/prompt_objects/universal/get_env_data.rb +64 -0
- data/lib/prompt_objects/universal/list_env_data.rb +61 -0
- data/lib/prompt_objects/universal/store_env_data.rb +87 -0
- data/lib/prompt_objects/universal/update_env_data.rb +88 -0
- data/lib/prompt_objects.rb +6 -1
- data/prompt_objects.gemspec +1 -1
- data/templates/arc-agi-1/objects/observer.md +4 -0
- data/templates/arc-agi-1/objects/solver.md +10 -1
- data/templates/arc-agi-1/objects/verifier.md +4 -0
- 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
- data/tools/thread-explorer.html +27 -0
- metadata +18 -16
- data/Gemfile.lock +0 -233
- data/IMPLEMENTATION_PLAN.md +0 -1073
- data/design-doc-v2.md +0 -1232
- data/frontend/src/components/CapabilitiesPanel.tsx +0 -141
- data/frontend/src/components/ChatPanel.tsx +0 -296
- data/frontend/src/components/Dashboard.tsx +0 -83
- data/frontend/src/components/Header.tsx +0 -153
- 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-6y64NXFy.css +0 -1
|
@@ -37,52 +37,52 @@ export function UsagePanel({ usage, onClose }: UsagePanelProps) {
|
|
|
37
37
|
return (
|
|
38
38
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
|
39
39
|
<div
|
|
40
|
-
className="bg-po-surface border border-po-border rounded
|
|
40
|
+
className="bg-po-surface-2 border border-po-border rounded shadow-2xl w-[400px] max-h-[80vh] overflow-auto"
|
|
41
41
|
onClick={(e) => e.stopPropagation()}
|
|
42
42
|
>
|
|
43
|
-
<div className="flex items-center justify-between
|
|
44
|
-
<h3 className="font-
|
|
45
|
-
Token Usage {usage.include_tree && <span className="text-
|
|
43
|
+
<div className="flex items-center justify-between px-4 py-2.5 border-b border-po-border">
|
|
44
|
+
<h3 className="font-mono text-xs text-po-text-primary">
|
|
45
|
+
Token Usage {usage.include_tree && <span className="text-po-text-ghost ml-1">(full tree)</span>}
|
|
46
46
|
</h3>
|
|
47
|
-
<button onClick={onClose} className="text-
|
|
48
|
-
|
|
47
|
+
<button onClick={onClose} className="text-po-text-ghost hover:text-po-text-primary transition-colors duration-150">
|
|
48
|
+
{'\u2715'}
|
|
49
49
|
</button>
|
|
50
50
|
</div>
|
|
51
51
|
|
|
52
52
|
<div className="p-4 space-y-4">
|
|
53
53
|
{/* Summary */}
|
|
54
|
-
<div className="grid grid-cols-3 gap-
|
|
55
|
-
<div className="bg-po-
|
|
56
|
-
<div className="text-
|
|
57
|
-
<div className="text-
|
|
54
|
+
<div className="grid grid-cols-3 gap-2">
|
|
55
|
+
<div className="bg-po-surface rounded p-2.5 text-center">
|
|
56
|
+
<div className="text-sm font-mono text-po-accent">{formatTokens(usage.input_tokens)}</div>
|
|
57
|
+
<div className="text-2xs text-po-text-ghost uppercase tracking-wider mt-0.5">Input</div>
|
|
58
58
|
</div>
|
|
59
|
-
<div className="bg-po-
|
|
60
|
-
<div className="text-
|
|
61
|
-
<div className="text-
|
|
59
|
+
<div className="bg-po-surface rounded p-2.5 text-center">
|
|
60
|
+
<div className="text-sm font-mono text-po-warning">{formatTokens(usage.output_tokens)}</div>
|
|
61
|
+
<div className="text-2xs text-po-text-ghost uppercase tracking-wider mt-0.5">Output</div>
|
|
62
62
|
</div>
|
|
63
|
-
<div className="bg-po-
|
|
64
|
-
<div className="text-
|
|
65
|
-
<div className="text-
|
|
63
|
+
<div className="bg-po-surface rounded p-2.5 text-center">
|
|
64
|
+
<div className="text-sm font-mono text-po-text-primary">{formatCost(usage.estimated_cost_usd)}</div>
|
|
65
|
+
<div className="text-2xs text-po-text-ghost uppercase tracking-wider mt-0.5">Est. Cost</div>
|
|
66
66
|
</div>
|
|
67
67
|
</div>
|
|
68
68
|
|
|
69
|
-
<div className="flex justify-between text-
|
|
70
|
-
<span>{usage.calls}
|
|
71
|
-
<span>{formatTokens(usage.total_tokens)} total
|
|
69
|
+
<div className="flex justify-between text-2xs text-po-text-ghost px-1 font-mono">
|
|
70
|
+
<span>{usage.calls} call{usage.calls !== 1 ? 's' : ''}</span>
|
|
71
|
+
<span>{formatTokens(usage.total_tokens)} total</span>
|
|
72
72
|
</div>
|
|
73
73
|
|
|
74
74
|
{/* Per-model breakdown */}
|
|
75
75
|
{models.length > 0 && (
|
|
76
76
|
<div>
|
|
77
|
-
<h4 className="text-
|
|
78
|
-
<div className="space-y-
|
|
77
|
+
<h4 className="text-2xs text-po-text-ghost uppercase tracking-wider mb-2">By Model</h4>
|
|
78
|
+
<div className="space-y-1.5">
|
|
79
79
|
{models.map(([model, data]) => (
|
|
80
|
-
<div key={model} className="bg-po-
|
|
80
|
+
<div key={model} className="bg-po-surface rounded p-2.5">
|
|
81
81
|
<div className="flex items-center justify-between mb-1">
|
|
82
|
-
<span className="text-xs font-mono text-
|
|
83
|
-
<span className="text-
|
|
82
|
+
<span className="text-xs font-mono text-po-text-primary">{model}</span>
|
|
83
|
+
<span className="text-2xs text-po-text-ghost">{data.calls} call{data.calls !== 1 ? 's' : ''}</span>
|
|
84
84
|
</div>
|
|
85
|
-
<div className="flex justify-between text-
|
|
85
|
+
<div className="flex justify-between text-2xs text-po-text-ghost font-mono">
|
|
86
86
|
<span>In: {formatTokens(data.input_tokens)}</span>
|
|
87
87
|
<span>Out: {formatTokens(data.output_tokens)}</span>
|
|
88
88
|
<span>{formatCost(data.estimated_cost_usd)}</span>
|
|
@@ -94,8 +94,8 @@ export function UsagePanel({ usage, onClose }: UsagePanelProps) {
|
|
|
94
94
|
)}
|
|
95
95
|
|
|
96
96
|
{usage.calls === 0 && (
|
|
97
|
-
<div className="text-center text-
|
|
98
|
-
No usage data recorded
|
|
97
|
+
<div className="text-center text-po-text-ghost text-xs py-4 font-mono">
|
|
98
|
+
No usage data recorded.
|
|
99
99
|
</div>
|
|
100
100
|
)}
|
|
101
101
|
</div>
|
|
@@ -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
|
+
}
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
Environment,
|
|
9
9
|
Message,
|
|
10
10
|
LLMConfig,
|
|
11
|
+
EnvDataEntry,
|
|
11
12
|
SendMessagePayload,
|
|
12
13
|
RespondToNotificationPayload,
|
|
13
14
|
CreateSessionPayload,
|
|
@@ -43,6 +44,8 @@ export function useWebSocket() {
|
|
|
43
44
|
setLLMConfig,
|
|
44
45
|
updateCurrentLLM,
|
|
45
46
|
setUsageData,
|
|
47
|
+
setEnvData,
|
|
48
|
+
setSessionRoot,
|
|
46
49
|
} = useStore()
|
|
47
50
|
|
|
48
51
|
// Keep the handler ref up to date every render
|
|
@@ -215,6 +218,57 @@ export function useWebSocket() {
|
|
|
215
218
|
break
|
|
216
219
|
}
|
|
217
220
|
|
|
221
|
+
case 'prompt_updated': {
|
|
222
|
+
const { target, success } = message.payload as { target: string; success: boolean }
|
|
223
|
+
if (!success) console.warn('Prompt update failed for:', target)
|
|
224
|
+
break
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
case 'llm_error': {
|
|
228
|
+
const { po_name, error, provider, model } = message.payload as {
|
|
229
|
+
po_name: string
|
|
230
|
+
provider: string
|
|
231
|
+
model: string
|
|
232
|
+
error: string
|
|
233
|
+
error_class: string
|
|
234
|
+
}
|
|
235
|
+
console.error(`LLM error for ${po_name} (${provider}/${model}):`, error)
|
|
236
|
+
// Reset PO to idle so UI isn't stuck in "thinking" state
|
|
237
|
+
setPromptObject(po_name, { status: 'idle' })
|
|
238
|
+
break
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case 'session_created':
|
|
242
|
+
// Session creation confirmed — po_state update follows with full state
|
|
243
|
+
break
|
|
244
|
+
|
|
245
|
+
case 'session_switched':
|
|
246
|
+
// Session switch confirmed — po_state update follows with full state
|
|
247
|
+
break
|
|
248
|
+
|
|
249
|
+
case 'env_data_changed': {
|
|
250
|
+
const { root_thread_id, entries } = message.payload as {
|
|
251
|
+
action: string
|
|
252
|
+
root_thread_id: string
|
|
253
|
+
key: string
|
|
254
|
+
stored_by: string
|
|
255
|
+
entries: EnvDataEntry[]
|
|
256
|
+
}
|
|
257
|
+
setEnvData(root_thread_id, entries)
|
|
258
|
+
break
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
case 'env_data_list': {
|
|
262
|
+
const { session_id, root_thread_id, entries } = message.payload as {
|
|
263
|
+
session_id: string
|
|
264
|
+
root_thread_id: string
|
|
265
|
+
entries: EnvDataEntry[]
|
|
266
|
+
}
|
|
267
|
+
setEnvData(root_thread_id, entries)
|
|
268
|
+
setSessionRoot(session_id, root_thread_id)
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
|
|
218
272
|
case 'error': {
|
|
219
273
|
const { message: errorMsg } = message.payload as { message: string }
|
|
220
274
|
console.error('Server error:', errorMsg)
|
|
@@ -460,6 +514,21 @@ export function useWebSocket() {
|
|
|
460
514
|
)
|
|
461
515
|
}, [])
|
|
462
516
|
|
|
517
|
+
// Request env data for a session
|
|
518
|
+
const requestEnvData = useCallback((sessionId: string) => {
|
|
519
|
+
if (!ws.current || ws.current.readyState !== WebSocket.OPEN) {
|
|
520
|
+
console.error('WebSocket not connected')
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
ws.current.send(
|
|
525
|
+
JSON.stringify({
|
|
526
|
+
type: 'get_env_data_list',
|
|
527
|
+
payload: { session_id: sessionId },
|
|
528
|
+
})
|
|
529
|
+
)
|
|
530
|
+
}, [])
|
|
531
|
+
|
|
463
532
|
// Update a PO's prompt (markdown body)
|
|
464
533
|
const updatePrompt = useCallback((target: string, prompt: string) => {
|
|
465
534
|
if (!ws.current || ws.current.readyState !== WebSocket.OPEN) {
|
|
@@ -485,5 +554,6 @@ export function useWebSocket() {
|
|
|
485
554
|
updatePrompt,
|
|
486
555
|
requestUsage,
|
|
487
556
|
exportThread,
|
|
557
|
+
requestEnvData,
|
|
488
558
|
}
|
|
489
559
|
}
|
data/frontend/src/index.css
CHANGED
|
@@ -13,8 +13,8 @@ body {
|
|
|
13
13
|
|
|
14
14
|
/* Custom scrollbar */
|
|
15
15
|
::-webkit-scrollbar {
|
|
16
|
-
width:
|
|
17
|
-
height:
|
|
16
|
+
width: 6px;
|
|
17
|
+
height: 6px;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
::-webkit-scrollbar-track {
|
|
@@ -26,16 +26,31 @@ body {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
::-webkit-scrollbar-thumb:hover {
|
|
29
|
-
@apply bg-po-
|
|
29
|
+
@apply bg-po-border-focus;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Selection */
|
|
33
|
+
::selection {
|
|
34
|
+
background: rgba(212, 149, 42, 0.3);
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
/* Custom utilities */
|
|
33
38
|
@layer utilities {
|
|
34
39
|
.scrollbar-thin {
|
|
35
40
|
scrollbar-width: thin;
|
|
41
|
+
scrollbar-color: #3d3a37 #1a1918;
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
/* Resize handles */
|
|
46
|
+
.resize-handle {
|
|
47
|
+
@apply w-1 cursor-col-resize hover:bg-po-accent/30 active:bg-po-accent/50 transition-colors border-l border-po-border;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.resize-handle-h {
|
|
51
|
+
@apply h-1 cursor-row-resize hover:bg-po-accent/30 active:bg-po-accent/50 transition-colors border-t border-po-border;
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
/* Canvas CSS2DRenderer labels */
|
|
40
55
|
.canvas-node-label {
|
|
41
56
|
text-align: center;
|
|
@@ -45,16 +60,18 @@ body {
|
|
|
45
60
|
|
|
46
61
|
.canvas-node-name {
|
|
47
62
|
display: block;
|
|
48
|
-
color: #
|
|
63
|
+
color: #e8e2da;
|
|
49
64
|
font-size: 12px;
|
|
50
65
|
font-weight: 500;
|
|
66
|
+
font-family: 'Geist Mono', 'IBM Plex Mono', monospace;
|
|
51
67
|
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
.canvas-node-status {
|
|
55
71
|
display: block;
|
|
56
|
-
color: #
|
|
72
|
+
color: #78726a;
|
|
57
73
|
font-size: 10px;
|
|
74
|
+
font-family: 'Geist Mono', 'IBM Plex Mono', monospace;
|
|
58
75
|
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
|
|
59
76
|
}
|
|
60
77
|
|
|
@@ -64,20 +81,20 @@ body {
|
|
|
64
81
|
justify-content: center;
|
|
65
82
|
width: 20px;
|
|
66
83
|
height: 20px;
|
|
67
|
-
background: #
|
|
68
|
-
color: #
|
|
84
|
+
background: #d4952a;
|
|
85
|
+
color: #1a1918;
|
|
69
86
|
font-size: 10px;
|
|
70
87
|
font-weight: 700;
|
|
71
88
|
border-radius: 50%;
|
|
72
89
|
pointer-events: none;
|
|
73
90
|
user-select: none;
|
|
74
|
-
box-shadow: 0 0 8px rgba(
|
|
91
|
+
box-shadow: 0 0 8px rgba(212, 149, 42, 0.6);
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
.canvas-toolcall-label {
|
|
78
|
-
color: #
|
|
95
|
+
color: #3b9a6e;
|
|
79
96
|
font-size: 10px;
|
|
80
|
-
font-family: monospace;
|
|
97
|
+
font-family: 'Geist Mono', 'IBM Plex Mono', monospace;
|
|
81
98
|
text-align: center;
|
|
82
99
|
pointer-events: none;
|
|
83
100
|
user-select: none;
|