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.
Files changed (65) hide show
  1. package/cli/index.ts +882 -0
  2. package/index.ts +3 -0
  3. package/package.json +53 -0
  4. package/server/connectorBinaries.ts +103 -0
  5. package/server/connectorRegistry.ts +158 -0
  6. package/server/conversation/adapterRegistry.ts +53 -0
  7. package/server/conversation/adapters/claudeAdapter.ts +138 -0
  8. package/server/conversation/adapters/codexAdapter.ts +142 -0
  9. package/server/conversation/adapters.ts +224 -0
  10. package/server/conversation/executeRunner.ts +1191 -0
  11. package/server/conversation/gitWorkflow.ts +1037 -0
  12. package/server/conversation/models.ts +23 -0
  13. package/server/conversation/pragmaCli.ts +34 -0
  14. package/server/conversation/prompts.ts +335 -0
  15. package/server/conversation/store.ts +805 -0
  16. package/server/conversation/titleGenerator.ts +106 -0
  17. package/server/conversation/turnRunner.ts +365 -0
  18. package/server/conversation/types.ts +134 -0
  19. package/server/db.ts +837 -0
  20. package/server/http/middleware.ts +31 -0
  21. package/server/http/schemas.ts +430 -0
  22. package/server/http/validators.ts +38 -0
  23. package/server/index.ts +6560 -0
  24. package/server/process/runCommand.ts +142 -0
  25. package/server/stores/agentStore.ts +167 -0
  26. package/server/stores/connectorStore.ts +299 -0
  27. package/server/stores/humanStore.ts +28 -0
  28. package/server/stores/skillStore.ts +127 -0
  29. package/server/stores/taskStore.ts +371 -0
  30. package/shared/net.ts +24 -0
  31. package/tsconfig.json +14 -0
  32. package/ui/index.html +14 -0
  33. package/ui/public/favicon-32.png +0 -0
  34. package/ui/public/favicon.png +0 -0
  35. package/ui/src/App.jsx +1338 -0
  36. package/ui/src/api.js +954 -0
  37. package/ui/src/components/CodeView.jsx +319 -0
  38. package/ui/src/components/ConnectionsView.jsx +1004 -0
  39. package/ui/src/components/ContextView.jsx +315 -0
  40. package/ui/src/components/ConversationDrawer.jsx +963 -0
  41. package/ui/src/components/EmptyPane.jsx +20 -0
  42. package/ui/src/components/FeedView.jsx +773 -0
  43. package/ui/src/components/FilesView.jsx +257 -0
  44. package/ui/src/components/InlineChatView.jsx +158 -0
  45. package/ui/src/components/InputBar.jsx +476 -0
  46. package/ui/src/components/OnboardingModal.jsx +112 -0
  47. package/ui/src/components/OutputPanel.jsx +658 -0
  48. package/ui/src/components/PlanProposalPanel.jsx +177 -0
  49. package/ui/src/components/RightPanel.jsx +951 -0
  50. package/ui/src/components/SettingsView.jsx +186 -0
  51. package/ui/src/components/Sidebar.jsx +247 -0
  52. package/ui/src/components/TestingPane.jsx +198 -0
  53. package/ui/src/components/testing/ApiTesterPanel.jsx +187 -0
  54. package/ui/src/components/testing/LogViewerPanel.jsx +64 -0
  55. package/ui/src/components/testing/TerminalPanel.jsx +104 -0
  56. package/ui/src/components/testing/WebPreviewPanel.jsx +78 -0
  57. package/ui/src/hooks/useAgents.js +81 -0
  58. package/ui/src/hooks/useConversation.js +252 -0
  59. package/ui/src/hooks/useTasks.js +161 -0
  60. package/ui/src/hooks/useWorkspace.js +259 -0
  61. package/ui/src/lib/agentIcon.js +10 -0
  62. package/ui/src/lib/conversationUtils.js +575 -0
  63. package/ui/src/main.jsx +10 -0
  64. package/ui/src/styles.css +6899 -0
  65. 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
+ }