pragma-so 0.1.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.
- package/cli/index.ts +882 -0
- package/index.ts +3 -0
- package/package.json +53 -0
- package/server/connectorBinaries.ts +103 -0
- package/server/connectorRegistry.ts +158 -0
- package/server/conversation/adapterRegistry.ts +53 -0
- package/server/conversation/adapters/claudeAdapter.ts +138 -0
- package/server/conversation/adapters/codexAdapter.ts +142 -0
- package/server/conversation/adapters.ts +224 -0
- package/server/conversation/executeRunner.ts +1191 -0
- package/server/conversation/gitWorkflow.ts +1037 -0
- package/server/conversation/models.ts +23 -0
- package/server/conversation/pragmaCli.ts +34 -0
- package/server/conversation/prompts.ts +335 -0
- package/server/conversation/store.ts +805 -0
- package/server/conversation/titleGenerator.ts +106 -0
- package/server/conversation/turnRunner.ts +365 -0
- package/server/conversation/types.ts +134 -0
- package/server/db.ts +837 -0
- package/server/http/middleware.ts +31 -0
- package/server/http/schemas.ts +430 -0
- package/server/http/validators.ts +38 -0
- package/server/index.ts +6560 -0
- package/server/process/runCommand.ts +142 -0
- package/server/stores/agentStore.ts +167 -0
- package/server/stores/connectorStore.ts +299 -0
- package/server/stores/humanStore.ts +28 -0
- package/server/stores/skillStore.ts +127 -0
- package/server/stores/taskStore.ts +371 -0
- package/shared/net.ts +24 -0
- package/tsconfig.json +14 -0
- package/ui/index.html +14 -0
- package/ui/public/favicon-32.png +0 -0
- package/ui/public/favicon.png +0 -0
- package/ui/src/App.jsx +1338 -0
- package/ui/src/api.js +954 -0
- package/ui/src/components/CodeView.jsx +319 -0
- package/ui/src/components/ConnectionsView.jsx +1004 -0
- package/ui/src/components/ContextView.jsx +315 -0
- package/ui/src/components/ConversationDrawer.jsx +963 -0
- package/ui/src/components/EmptyPane.jsx +20 -0
- package/ui/src/components/FeedView.jsx +773 -0
- package/ui/src/components/FilesView.jsx +257 -0
- package/ui/src/components/InlineChatView.jsx +158 -0
- package/ui/src/components/InputBar.jsx +476 -0
- package/ui/src/components/OnboardingModal.jsx +112 -0
- package/ui/src/components/OutputPanel.jsx +658 -0
- package/ui/src/components/PlanProposalPanel.jsx +177 -0
- package/ui/src/components/RightPanel.jsx +951 -0
- package/ui/src/components/SettingsView.jsx +186 -0
- package/ui/src/components/Sidebar.jsx +247 -0
- package/ui/src/components/TestingPane.jsx +198 -0
- package/ui/src/components/testing/ApiTesterPanel.jsx +187 -0
- package/ui/src/components/testing/LogViewerPanel.jsx +64 -0
- package/ui/src/components/testing/TerminalPanel.jsx +104 -0
- package/ui/src/components/testing/WebPreviewPanel.jsx +78 -0
- package/ui/src/hooks/useAgents.js +81 -0
- package/ui/src/hooks/useConversation.js +252 -0
- package/ui/src/hooks/useTasks.js +161 -0
- package/ui/src/hooks/useWorkspace.js +259 -0
- package/ui/src/lib/agentIcon.js +10 -0
- package/ui/src/lib/conversationUtils.js +575 -0
- package/ui/src/main.jsx +10 -0
- package/ui/src/styles.css +6899 -0
- package/ui/vite.config.mjs +6 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { proxyTestingRequest } from '../../api'
|
|
3
|
+
|
|
4
|
+
function statusClass(code) {
|
|
5
|
+
if (code >= 200 && code < 300) return 'success'
|
|
6
|
+
if (code >= 400 && code < 500) return 'client-error'
|
|
7
|
+
return 'server-error'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function hasBody(method) {
|
|
11
|
+
return ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method?.toUpperCase())
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function EndpointCard({ taskId, endpoint, panel, services, processStatuses, defaultExpanded }) {
|
|
15
|
+
const [expanded, setExpanded] = useState(defaultExpanded)
|
|
16
|
+
const [bodyText, setBodyText] = useState(() => {
|
|
17
|
+
if (endpoint.body == null) return ''
|
|
18
|
+
if (typeof endpoint.body === 'string') return endpoint.body
|
|
19
|
+
return JSON.stringify(endpoint.body, null, 2)
|
|
20
|
+
})
|
|
21
|
+
const [headersEntries, setHeadersEntries] = useState(() => {
|
|
22
|
+
if (endpoint.headers && typeof endpoint.headers === 'object') {
|
|
23
|
+
return Object.entries(endpoint.headers).map(([k, v]) => ({ key: k, value: v }))
|
|
24
|
+
}
|
|
25
|
+
return []
|
|
26
|
+
})
|
|
27
|
+
const [responseData, setResponseData] = useState(null)
|
|
28
|
+
const [responseLoading, setResponseLoading] = useState(false)
|
|
29
|
+
const [responseError, setResponseError] = useState('')
|
|
30
|
+
|
|
31
|
+
const method = (endpoint.method || 'GET').toUpperCase()
|
|
32
|
+
const path = endpoint.path || '/'
|
|
33
|
+
const description = endpoint.description || ''
|
|
34
|
+
|
|
35
|
+
const processName = panel.process || ''
|
|
36
|
+
const svc = services[processName]
|
|
37
|
+
const svcStatus = svc?.id ? (processStatuses[svc.id] || svc.status) : null
|
|
38
|
+
const isReady = svcStatus === 'ready'
|
|
39
|
+
|
|
40
|
+
function addHeader() {
|
|
41
|
+
setHeadersEntries(prev => [...prev, { key: '', value: '' }])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function removeHeader(index) {
|
|
45
|
+
setHeadersEntries(prev => prev.filter((_, i) => i !== index))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function updateHeader(index, field, value) {
|
|
49
|
+
setHeadersEntries(prev => prev.map((h, i) => i === index ? { ...h, [field]: value } : h))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function handleSend() {
|
|
53
|
+
if (responseLoading || !isReady) return
|
|
54
|
+
setResponseLoading(true)
|
|
55
|
+
setResponseError('')
|
|
56
|
+
setResponseData(null)
|
|
57
|
+
|
|
58
|
+
const hdrs = {}
|
|
59
|
+
for (const h of headersEntries) {
|
|
60
|
+
if (h.key.trim()) hdrs[h.key.trim()] = h.value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const result = await proxyTestingRequest(taskId, {
|
|
65
|
+
process_name: processName,
|
|
66
|
+
method,
|
|
67
|
+
path,
|
|
68
|
+
headers: Object.keys(hdrs).length > 0 ? hdrs : undefined,
|
|
69
|
+
body: hasBody(method) && bodyText.trim() ? bodyText.trim() : undefined,
|
|
70
|
+
})
|
|
71
|
+
setResponseData(result)
|
|
72
|
+
} catch (err) {
|
|
73
|
+
setResponseError(err instanceof Error ? err.message : String(err))
|
|
74
|
+
} finally {
|
|
75
|
+
setResponseLoading(false)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="api-endpoint-card">
|
|
81
|
+
<div className="api-endpoint-header" onClick={() => setExpanded(v => !v)}>
|
|
82
|
+
<span className={`api-method-badge ${method.toLowerCase()}`}>{method}</span>
|
|
83
|
+
<span className="api-endpoint-path">{path}</span>
|
|
84
|
+
{description && <span className="api-endpoint-desc">{description}</span>}
|
|
85
|
+
<span style={{ marginLeft: 'auto', opacity: 0.4, fontSize: 11 }}>{expanded ? '▲' : '▼'}</span>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{expanded && (
|
|
89
|
+
<div className="api-endpoint-body">
|
|
90
|
+
<div className="api-headers-editor">
|
|
91
|
+
<div style={{ fontSize: 11, fontWeight: 600, marginBottom: 4, opacity: 0.7 }}>Headers</div>
|
|
92
|
+
{headersEntries.map((h, i) => (
|
|
93
|
+
<div key={i} className="api-header-row">
|
|
94
|
+
<input
|
|
95
|
+
className="api-header-input"
|
|
96
|
+
placeholder="Key"
|
|
97
|
+
value={h.key}
|
|
98
|
+
onChange={e => updateHeader(i, 'key', e.target.value)}
|
|
99
|
+
/>
|
|
100
|
+
<input
|
|
101
|
+
className="api-header-input"
|
|
102
|
+
placeholder="Value"
|
|
103
|
+
value={h.value}
|
|
104
|
+
onChange={e => updateHeader(i, 'value', e.target.value)}
|
|
105
|
+
/>
|
|
106
|
+
<button
|
|
107
|
+
className="api-header-remove-btn"
|
|
108
|
+
onClick={() => removeHeader(i)}
|
|
109
|
+
title="Remove header"
|
|
110
|
+
>
|
|
111
|
+
x
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
))}
|
|
115
|
+
<button className="api-header-add-btn" onClick={addHeader}>+ Add Header</button>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{hasBody(method) && (
|
|
119
|
+
<div style={{ marginTop: 8 }}>
|
|
120
|
+
<div style={{ fontSize: 11, fontWeight: 600, marginBottom: 4, opacity: 0.7 }}>Body</div>
|
|
121
|
+
<textarea
|
|
122
|
+
className="api-body-editor"
|
|
123
|
+
value={bodyText}
|
|
124
|
+
onChange={e => setBodyText(e.target.value)}
|
|
125
|
+
placeholder='{ "key": "value" }'
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
<button
|
|
131
|
+
className="api-send-btn"
|
|
132
|
+
onClick={handleSend}
|
|
133
|
+
disabled={responseLoading || !isReady}
|
|
134
|
+
>
|
|
135
|
+
{responseLoading ? 'Sending...' : !isReady ? 'Waiting for server...' : 'Send Request'}
|
|
136
|
+
</button>
|
|
137
|
+
|
|
138
|
+
{responseError && (
|
|
139
|
+
<div className="error" style={{ marginTop: 8, fontSize: 12 }}>Error: {responseError}</div>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{responseData && (
|
|
143
|
+
<div className="api-response">
|
|
144
|
+
<div className="api-response-status">
|
|
145
|
+
<span className={`api-response-status-code ${statusClass(responseData.status)}`}>
|
|
146
|
+
{responseData.status}
|
|
147
|
+
</span>
|
|
148
|
+
{responseData.elapsed_ms != null && (
|
|
149
|
+
<span style={{ opacity: 0.6 }}>{responseData.elapsed_ms}ms</span>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
<pre className="api-response-body">
|
|
153
|
+
{typeof responseData.body === 'string'
|
|
154
|
+
? responseData.body
|
|
155
|
+
: JSON.stringify(responseData.body, null, 2)}
|
|
156
|
+
</pre>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function ApiTesterPanel({ taskId, panel, services, processStatuses }) {
|
|
166
|
+
const endpoints = Array.isArray(panel?.endpoints) ? panel.endpoints : []
|
|
167
|
+
|
|
168
|
+
if (endpoints.length === 0) {
|
|
169
|
+
return <div className="muted" style={{ padding: 12 }}>No endpoints configured.</div>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div className="api-tester">
|
|
174
|
+
{endpoints.map((ep, i) => (
|
|
175
|
+
<EndpointCard
|
|
176
|
+
key={`${ep.method}-${ep.path}-${i}`}
|
|
177
|
+
taskId={taskId}
|
|
178
|
+
endpoint={ep}
|
|
179
|
+
panel={panel}
|
|
180
|
+
services={services}
|
|
181
|
+
processStatuses={processStatuses}
|
|
182
|
+
defaultExpanded={i === 0}
|
|
183
|
+
/>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export function LogViewerPanel({ panel, services, processLogs }) {
|
|
4
|
+
const [filter, setFilter] = useState('all')
|
|
5
|
+
const [autoScroll, setAutoScroll] = useState(true)
|
|
6
|
+
const contentRef = useRef(null)
|
|
7
|
+
|
|
8
|
+
const processName = panel.process || ''
|
|
9
|
+
const svc = services[processName]
|
|
10
|
+
const serviceId = svc?.id || null
|
|
11
|
+
|
|
12
|
+
const rawLogs = serviceId && processLogs[serviceId] ? processLogs[serviceId] : []
|
|
13
|
+
|
|
14
|
+
const filteredLogs = filter === 'all'
|
|
15
|
+
? rawLogs
|
|
16
|
+
: rawLogs.filter(entry => (entry?.stream || 'stdout') === filter)
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (autoScroll && contentRef.current) {
|
|
20
|
+
contentRef.current.scrollTop = contentRef.current.scrollHeight
|
|
21
|
+
}
|
|
22
|
+
}, [filteredLogs.length, autoScroll])
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="log-viewer">
|
|
26
|
+
<div className="log-viewer-filters">
|
|
27
|
+
{['all', 'stdout', 'stderr', 'system'].map(f => (
|
|
28
|
+
<button
|
|
29
|
+
key={f}
|
|
30
|
+
className={`log-viewer-filter-btn ${filter === f ? 'active' : ''}`}
|
|
31
|
+
onClick={() => setFilter(f)}
|
|
32
|
+
>
|
|
33
|
+
{f === 'all' ? 'All' : f}
|
|
34
|
+
</button>
|
|
35
|
+
))}
|
|
36
|
+
<button
|
|
37
|
+
className={`log-viewer-filter-btn ${autoScroll ? 'active' : ''}`}
|
|
38
|
+
onClick={() => setAutoScroll(v => !v)}
|
|
39
|
+
style={{ marginLeft: 'auto' }}
|
|
40
|
+
>
|
|
41
|
+
Auto-scroll
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className="log-viewer-content" ref={contentRef}>
|
|
46
|
+
{filteredLogs.length === 0 ? (
|
|
47
|
+
<div className="muted" style={{ fontSize: 12 }}>No logs yet.</div>
|
|
48
|
+
) : (
|
|
49
|
+
filteredLogs.map((entry, i) => {
|
|
50
|
+
const stream = entry?.stream || 'stdout'
|
|
51
|
+
const text = entry?.text || ''
|
|
52
|
+
const ts = entry?.timestamp ? new Date(entry.timestamp).toLocaleTimeString() : ''
|
|
53
|
+
return (
|
|
54
|
+
<div key={i} className={`log-entry log-entry-${stream}`}>
|
|
55
|
+
{ts && <span style={{ opacity: 0.5, marginRight: 8 }}>[{ts}]</span>}
|
|
56
|
+
{text}
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { sendServiceStdin, openRuntimeServiceStream } from '../../api'
|
|
3
|
+
|
|
4
|
+
export function TerminalPanel({ taskId, panel, services, processStatuses }) {
|
|
5
|
+
const [logs, setLogs] = useState([])
|
|
6
|
+
const [inputValue, setInputValue] = useState('')
|
|
7
|
+
const outputRef = useRef(null)
|
|
8
|
+
const cleanupRef = useRef(null)
|
|
9
|
+
|
|
10
|
+
const processName = panel.process || ''
|
|
11
|
+
const svc = services[processName]
|
|
12
|
+
const serviceId = svc?.id || null
|
|
13
|
+
const svcStatus = serviceId ? (processStatuses[serviceId] || svc?.status) : null
|
|
14
|
+
|
|
15
|
+
const suggestedInputs = Array.isArray(panel?.suggested_inputs) ? panel.suggested_inputs : []
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!serviceId) return
|
|
19
|
+
|
|
20
|
+
setLogs([])
|
|
21
|
+
const cleanup = openRuntimeServiceStream(serviceId, {
|
|
22
|
+
onLog(entry) {
|
|
23
|
+
if (!entry) return
|
|
24
|
+
setLogs(prev => [...prev, entry])
|
|
25
|
+
},
|
|
26
|
+
onStatus() {},
|
|
27
|
+
onReady() {},
|
|
28
|
+
})
|
|
29
|
+
cleanupRef.current = cleanup
|
|
30
|
+
|
|
31
|
+
return () => {
|
|
32
|
+
cleanup()
|
|
33
|
+
cleanupRef.current = null
|
|
34
|
+
}
|
|
35
|
+
}, [serviceId])
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (outputRef.current) {
|
|
39
|
+
outputRef.current.scrollTop = outputRef.current.scrollHeight
|
|
40
|
+
}
|
|
41
|
+
}, [logs])
|
|
42
|
+
|
|
43
|
+
async function handleSend(text) {
|
|
44
|
+
if (!serviceId || !text) return
|
|
45
|
+
try {
|
|
46
|
+
await sendServiceStdin(serviceId, text + '\n')
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error('Failed to send stdin:', err)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleKeyDown(e) {
|
|
53
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
54
|
+
e.preventDefault()
|
|
55
|
+
handleSend(inputValue)
|
|
56
|
+
setInputValue('')
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="terminal-panel">
|
|
62
|
+
<pre className="terminal-output" ref={outputRef}>
|
|
63
|
+
{logs.length === 0
|
|
64
|
+
? (svcStatus ? 'Waiting for output...' : 'Process not started.')
|
|
65
|
+
: logs.map((entry, i) => {
|
|
66
|
+
const text = entry?.text || ''
|
|
67
|
+
const stream = entry?.stream || 'stdout'
|
|
68
|
+
return (
|
|
69
|
+
<span key={i} className={stream === 'stderr' ? 'log-entry-stderr' : ''}>
|
|
70
|
+
{text}
|
|
71
|
+
</span>
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
</pre>
|
|
76
|
+
|
|
77
|
+
{suggestedInputs.length > 0 && (
|
|
78
|
+
<div className="terminal-suggested">
|
|
79
|
+
{suggestedInputs.map((input, i) => (
|
|
80
|
+
<button
|
|
81
|
+
key={i}
|
|
82
|
+
className="terminal-suggested-btn"
|
|
83
|
+
onClick={() => handleSend(input)}
|
|
84
|
+
disabled={!serviceId}
|
|
85
|
+
>
|
|
86
|
+
{input}
|
|
87
|
+
</button>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
<div className="terminal-input-row">
|
|
93
|
+
<input
|
|
94
|
+
className="terminal-input"
|
|
95
|
+
value={inputValue}
|
|
96
|
+
onChange={e => setInputValue(e.target.value)}
|
|
97
|
+
onKeyDown={handleKeyDown}
|
|
98
|
+
placeholder={serviceId ? 'Type a command...' : 'Process not started'}
|
|
99
|
+
disabled={!serviceId}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export function WebPreviewPanel({ panel, services, config, processStatuses }) {
|
|
4
|
+
const [device, setDevice] = useState('desktop')
|
|
5
|
+
const [iframeKey, setIframeKey] = useState(0)
|
|
6
|
+
|
|
7
|
+
const processName = panel.process || ''
|
|
8
|
+
const svc = services[processName]
|
|
9
|
+
const svcStatus = svc?.id ? (processStatuses[svc.id] || svc.status) : null
|
|
10
|
+
const isReady = svcStatus === 'ready'
|
|
11
|
+
|
|
12
|
+
const processes = Array.isArray(config?.processes) ? config.processes : []
|
|
13
|
+
const processConfig = processes.find(p => p.name === processName)
|
|
14
|
+
const port = processConfig?.port || 3000
|
|
15
|
+
const path = panel.path || '/'
|
|
16
|
+
const url = `http://localhost:${port}${path}`
|
|
17
|
+
|
|
18
|
+
function handleRefresh() {
|
|
19
|
+
setIframeKey(k => k + 1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function handleOpenExternal() {
|
|
23
|
+
window.open(url, '_blank')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="web-preview">
|
|
28
|
+
<div className="web-preview-toolbar">
|
|
29
|
+
<button
|
|
30
|
+
className={`web-preview-device-btn ${device === 'desktop' ? 'active' : ''}`}
|
|
31
|
+
onClick={() => setDevice('desktop')}
|
|
32
|
+
title="Desktop"
|
|
33
|
+
>
|
|
34
|
+
Desktop
|
|
35
|
+
</button>
|
|
36
|
+
<button
|
|
37
|
+
className={`web-preview-device-btn ${device === 'tablet' ? 'active' : ''}`}
|
|
38
|
+
onClick={() => setDevice('tablet')}
|
|
39
|
+
title="Tablet"
|
|
40
|
+
>
|
|
41
|
+
Tablet
|
|
42
|
+
</button>
|
|
43
|
+
<button
|
|
44
|
+
className={`web-preview-device-btn ${device === 'mobile' ? 'active' : ''}`}
|
|
45
|
+
onClick={() => setDevice('mobile')}
|
|
46
|
+
title="Mobile"
|
|
47
|
+
>
|
|
48
|
+
Mobile
|
|
49
|
+
</button>
|
|
50
|
+
<button className="web-preview-device-btn" onClick={handleRefresh} title="Refresh">
|
|
51
|
+
Refresh
|
|
52
|
+
</button>
|
|
53
|
+
<button className="web-preview-device-btn" onClick={handleOpenExternal} title="Open in new tab">
|
|
54
|
+
Open
|
|
55
|
+
</button>
|
|
56
|
+
<span className="web-preview-url">{url}</span>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div className="web-preview-container">
|
|
60
|
+
{!isReady ? (
|
|
61
|
+
<div className="web-preview-waiting">
|
|
62
|
+
<span className="conv-thinking-dot" />
|
|
63
|
+
<span style={{ marginLeft: 8 }}>Waiting for server to be ready...</span>
|
|
64
|
+
</div>
|
|
65
|
+
) : (
|
|
66
|
+
<div className={`web-preview-frame ${device}`}>
|
|
67
|
+
<iframe
|
|
68
|
+
key={iframeKey}
|
|
69
|
+
className="web-preview-iframe"
|
|
70
|
+
src={url}
|
|
71
|
+
title="Web Preview"
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react'
|
|
2
|
+
import { ApiError, fetchAgents, fetchHumans } from '../api'
|
|
3
|
+
import { ORCHESTRATOR_AGENT_ID, errorText } from '../lib/conversationUtils'
|
|
4
|
+
|
|
5
|
+
export function useAgents() {
|
|
6
|
+
const [agents, setAgents] = useState([])
|
|
7
|
+
const [agentsLoading, setAgentsLoading] = useState(false)
|
|
8
|
+
const [agentsError, setAgentsError] = useState('')
|
|
9
|
+
const [humans, setHumans] = useState([])
|
|
10
|
+
|
|
11
|
+
const orchestratorRuntime = useMemo(() => {
|
|
12
|
+
return agents.find((agent) => agent?.id === ORCHESTRATOR_AGENT_ID) ?? null
|
|
13
|
+
}, [agents])
|
|
14
|
+
|
|
15
|
+
const recipientAgents = useMemo(() => {
|
|
16
|
+
return agents.filter((agent) => agent && agent.id && agent.id !== ORCHESTRATOR_AGENT_ID)
|
|
17
|
+
}, [agents])
|
|
18
|
+
|
|
19
|
+
const agentById = useMemo(() => {
|
|
20
|
+
const map = Object.create(null)
|
|
21
|
+
for (const agent of agents) {
|
|
22
|
+
if (!agent || typeof agent.id !== 'string' || !agent.id) continue
|
|
23
|
+
map[agent.id] = agent
|
|
24
|
+
}
|
|
25
|
+
return map
|
|
26
|
+
}, [agents])
|
|
27
|
+
|
|
28
|
+
async function loadAgents() {
|
|
29
|
+
setAgentsLoading(true)
|
|
30
|
+
setAgentsError('')
|
|
31
|
+
try {
|
|
32
|
+
setAgents(await fetchAgents())
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof ApiError && error.code === 'NO_ACTIVE_WORKSPACE') {
|
|
35
|
+
setAgents([])
|
|
36
|
+
setAgentsError('No active workspace.')
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
setAgentsError(errorText(error))
|
|
40
|
+
} finally {
|
|
41
|
+
setAgentsLoading(false)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function loadHumans() {
|
|
46
|
+
try {
|
|
47
|
+
setHumans(await fetchHumans())
|
|
48
|
+
} catch {
|
|
49
|
+
// Humans are non-critical; keep empty list on failure.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function resolveOrchestratorRuntime() {
|
|
54
|
+
if (orchestratorRuntime) return orchestratorRuntime
|
|
55
|
+
try {
|
|
56
|
+
const latestAgents = await fetchAgents()
|
|
57
|
+
setAgents(latestAgents)
|
|
58
|
+
const runtime = latestAgents.find((agent) => agent?.id === ORCHESTRATOR_AGENT_ID) ?? null
|
|
59
|
+
if (runtime) return runtime
|
|
60
|
+
setAgentsError('Orchestrator agent is missing from this workspace.')
|
|
61
|
+
return null
|
|
62
|
+
} catch (error) {
|
|
63
|
+
setAgentsError(errorText(error))
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function clearAgentsData() {
|
|
69
|
+
setAgents([])
|
|
70
|
+
setHumans([])
|
|
71
|
+
setAgentsError('')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
agents, setAgents,
|
|
76
|
+
agentsLoading, agentsError, setAgentsError,
|
|
77
|
+
humans, setHumans,
|
|
78
|
+
orchestratorRuntime, recipientAgents, agentById,
|
|
79
|
+
loadAgents, loadHumans, resolveOrchestratorRuntime, clearAgentsData,
|
|
80
|
+
}
|
|
81
|
+
}
|