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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/CLAUDE.md +113 -44
  4. data/README.md +140 -14
  5. data/frontend/index.html +5 -1
  6. data/frontend/src/App.tsx +72 -79
  7. data/frontend/src/canvas/CanvasView.tsx +5 -5
  8. data/frontend/src/canvas/constants.ts +31 -31
  9. data/frontend/src/canvas/inspector/InspectorPanel.tsx +4 -4
  10. data/frontend/src/canvas/inspector/POInspector.tsx +35 -35
  11. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +13 -13
  12. data/frontend/src/canvas/nodes/PONode.ts +2 -2
  13. data/frontend/src/components/ContextMenu.tsx +5 -4
  14. data/frontend/src/components/EnvDataPane.tsx +69 -0
  15. data/frontend/src/components/Inspector.tsx +263 -0
  16. data/frontend/src/components/MarkdownMessage.tsx +22 -20
  17. data/frontend/src/components/MethodList.tsx +90 -0
  18. data/frontend/src/components/ModelSelector.tsx +13 -14
  19. data/frontend/src/components/NotificationPanel.tsx +29 -33
  20. data/frontend/src/components/ObjectList.tsx +78 -0
  21. data/frontend/src/components/PaneSlot.tsx +30 -0
  22. data/frontend/src/components/SourcePane.tsx +202 -0
  23. data/frontend/src/components/SystemBar.tsx +74 -0
  24. data/frontend/src/components/Transcript.tsx +76 -0
  25. data/frontend/src/components/UsagePanel.tsx +27 -27
  26. data/frontend/src/components/Workspace.tsx +260 -0
  27. data/frontend/src/components/index.ts +10 -9
  28. data/frontend/src/hooks/useResize.ts +55 -0
  29. data/frontend/src/hooks/useWebSocket.ts +70 -0
  30. data/frontend/src/index.css +27 -10
  31. data/frontend/src/store/index.ts +36 -0
  32. data/frontend/src/types/index.ts +13 -0
  33. data/frontend/tailwind.config.js +28 -9
  34. data/lib/prompt_objects/capability.rb +23 -1
  35. data/lib/prompt_objects/connectors/mcp.rb +2 -16
  36. data/lib/prompt_objects/environment.rb +15 -0
  37. data/lib/prompt_objects/llm/openai_adapter.rb +22 -0
  38. data/lib/prompt_objects/mcp/tools/inspect_po.rb +1 -31
  39. data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +1 -6
  40. data/lib/prompt_objects/prompt_object.rb +239 -7
  41. data/lib/prompt_objects/server/api/routes.rb +16 -48
  42. data/lib/prompt_objects/server/app.rb +14 -0
  43. data/lib/prompt_objects/server/public/assets/{index-xvyeb-5Z.js → index-DEPawnfZ.js} +206 -206
  44. data/lib/prompt_objects/server/public/assets/index-oMrRce1m.css +1 -0
  45. data/lib/prompt_objects/server/public/index.html +7 -3
  46. data/lib/prompt_objects/server/websocket_handler.rb +41 -98
  47. data/lib/prompt_objects/server.rb +6 -62
  48. data/lib/prompt_objects/session/store.rb +176 -4
  49. data/lib/prompt_objects/universal/delete_env_data.rb +70 -0
  50. data/lib/prompt_objects/universal/get_env_data.rb +64 -0
  51. data/lib/prompt_objects/universal/list_env_data.rb +61 -0
  52. data/lib/prompt_objects/universal/store_env_data.rb +87 -0
  53. data/lib/prompt_objects/universal/update_env_data.rb +88 -0
  54. data/lib/prompt_objects.rb +6 -1
  55. data/prompt_objects.gemspec +1 -1
  56. data/templates/arc-agi-1/objects/observer.md +4 -0
  57. data/templates/arc-agi-1/objects/solver.md +10 -1
  58. data/templates/arc-agi-1/objects/verifier.md +4 -0
  59. data/templates/arc-agi-1/primitives/find_objects.rb +1 -1
  60. data/templates/arc-agi-1/primitives/grid_diff.rb +2 -2
  61. data/templates/arc-agi-1/primitives/grid_info.rb +1 -1
  62. data/templates/arc-agi-1/primitives/grid_transform.rb +1 -1
  63. data/templates/arc-agi-1/primitives/render_grid.rb +1 -0
  64. data/templates/arc-agi-1/primitives/test_solution.rb +3 -0
  65. data/tools/thread-explorer.html +27 -0
  66. metadata +18 -16
  67. data/Gemfile.lock +0 -233
  68. data/IMPLEMENTATION_PLAN.md +0 -1073
  69. data/design-doc-v2.md +0 -1232
  70. data/frontend/src/components/CapabilitiesPanel.tsx +0 -141
  71. data/frontend/src/components/ChatPanel.tsx +0 -296
  72. data/frontend/src/components/Dashboard.tsx +0 -83
  73. data/frontend/src/components/Header.tsx +0 -153
  74. data/frontend/src/components/MessageBus.tsx +0 -56
  75. data/frontend/src/components/POCard.tsx +0 -56
  76. data/frontend/src/components/PODetail.tsx +0 -124
  77. data/frontend/src/components/PromptPanel.tsx +0 -156
  78. data/frontend/src/components/SessionsPanel.tsx +0 -174
  79. data/frontend/src/components/ThreadsSidebar.tsx +0 -163
  80. 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-lg shadow-2xl w-[420px] max-h-[80vh] overflow-auto"
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 p-4 border-b border-po-border">
44
- <h3 className="font-medium text-white">
45
- Token Usage {usage.include_tree && <span className="text-xs text-gray-400 ml-1">(full tree)</span>}
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-gray-400 hover:text-white transition-colors">
48
- &times;
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-3">
55
- <div className="bg-po-bg rounded-lg p-3 text-center">
56
- <div className="text-lg font-mono text-po-accent">{formatTokens(usage.input_tokens)}</div>
57
- <div className="text-[10px] text-gray-500 uppercase tracking-wider">Input</div>
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-bg rounded-lg p-3 text-center">
60
- <div className="text-lg font-mono text-po-warning">{formatTokens(usage.output_tokens)}</div>
61
- <div className="text-[10px] text-gray-500 uppercase tracking-wider">Output</div>
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-bg rounded-lg p-3 text-center">
64
- <div className="text-lg font-mono text-white">{formatCost(usage.estimated_cost_usd)}</div>
65
- <div className="text-[10px] text-gray-500 uppercase tracking-wider">Est. Cost</div>
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-xs text-gray-400 px-1">
70
- <span>{usage.calls} LLM call{usage.calls !== 1 ? 's' : ''}</span>
71
- <span>{formatTokens(usage.total_tokens)} total tokens</span>
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-xs text-gray-500 uppercase tracking-wider mb-2">By Model</h4>
78
- <div className="space-y-2">
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-bg rounded-lg p-3">
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-white">{model}</span>
83
- <span className="text-xs text-gray-400">{data.calls} call{data.calls !== 1 ? 's' : ''}</span>
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-[10px] text-gray-500">
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-gray-500 text-sm py-4">
98
- No usage data recorded for this thread.
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">&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
+ }
@@ -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
  }
@@ -13,8 +13,8 @@ body {
13
13
 
14
14
  /* Custom scrollbar */
15
15
  ::-webkit-scrollbar {
16
- width: 8px;
17
- height: 8px;
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-accent;
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: #ffffff;
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: #6b7280;
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: #f59e0b;
68
- color: #000000;
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(245, 158, 11, 0.6);
91
+ box-shadow: 0 0 8px rgba(212, 149, 42, 0.6);
75
92
  }
76
93
 
77
94
  .canvas-toolcall-label {
78
- color: #93c5fd;
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;