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,257 @@
1
+ import { useEffect, useMemo, useState } from 'react'
2
+ import { FileCode2, FileImage, FileSpreadsheet, FileText, FileType2 } from 'lucide-react'
3
+ import Papa from 'papaparse'
4
+ import ReactMarkdown from 'react-markdown'
5
+ import remarkGfm from 'remark-gfm'
6
+ import {
7
+ fetchWorkspaceOutputFiles,
8
+ workspaceOutputContentUrl,
9
+ workspaceOutputDownloadUrl,
10
+ } from '../api'
11
+
12
+ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'])
13
+
14
+ function ext(path) {
15
+ const index = path.lastIndexOf('.')
16
+ if (index === -1) return ''
17
+ return path.slice(index).toLowerCase()
18
+ }
19
+
20
+ function previewKind(path) {
21
+ const extension = ext(path)
22
+ if (extension === '.md') return 'markdown'
23
+ if (extension === '.html' || extension === '.htm') return 'html'
24
+ if (extension === '.csv') return 'csv'
25
+ if (IMAGE_EXTENSIONS.has(extension)) return 'image'
26
+ return 'download'
27
+ }
28
+
29
+ function fileName(path) {
30
+ const segments = String(path || '').split('/').filter(Boolean)
31
+ return segments[segments.length - 1] || path
32
+ }
33
+
34
+ function fileIcon(path) {
35
+ const extension = ext(path)
36
+ if (extension === '.md' || extension === '.txt') return FileText
37
+ if (extension === '.html' || extension === '.htm' || extension === '.js' || extension === '.ts') return FileCode2
38
+ if (extension === '.csv') return FileSpreadsheet
39
+ if (IMAGE_EXTENSIONS.has(extension)) return FileImage
40
+ return FileType2
41
+ }
42
+
43
+ function formatBytes(value) {
44
+ if (!Number.isFinite(value) || value < 0) return ''
45
+ if (value < 1024) return `${value} B`
46
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`
47
+ return `${(value / (1024 * 1024)).toFixed(1)} MB`
48
+ }
49
+
50
+ function parseCsv(text) {
51
+ const parsed = Papa.parse(text, { skipEmptyLines: 'greedy' })
52
+ if (parsed.errors.length > 0) {
53
+ throw new Error(parsed.errors[0].message || 'Failed to parse CSV.')
54
+ }
55
+ if (!Array.isArray(parsed.data)) return []
56
+ return parsed.data.map((row) => (Array.isArray(row) ? row : [String(row)]))
57
+ }
58
+
59
+ export function FilesView() {
60
+ const [files, setFiles] = useState([])
61
+ const [loading, setLoading] = useState(false)
62
+ const [error, setError] = useState('')
63
+ const [selectedPath, setSelectedPath] = useState('')
64
+ const [previewText, setPreviewText] = useState('')
65
+ const [previewLoading, setPreviewLoading] = useState(false)
66
+ const [previewError, setPreviewError] = useState('')
67
+
68
+ useEffect(() => {
69
+ void loadFiles()
70
+ }, [])
71
+
72
+ async function loadFiles() {
73
+ setLoading(true)
74
+ setError('')
75
+ try {
76
+ const data = await fetchWorkspaceOutputFiles()
77
+ const nextFiles = Array.isArray(data.files) ? data.files : []
78
+ setFiles(nextFiles)
79
+ } catch (err) {
80
+ setError(err instanceof Error ? err.message : String(err))
81
+ setFiles([])
82
+ } finally {
83
+ setLoading(false)
84
+ }
85
+ }
86
+
87
+ useEffect(() => {
88
+ if (!files.length) {
89
+ setSelectedPath('')
90
+ return
91
+ }
92
+ setSelectedPath((current) => {
93
+ if (current && files.some((f) => f.path === current)) return current
94
+ return files[0].path
95
+ })
96
+ }, [files])
97
+
98
+ const selectedFile = useMemo(() => {
99
+ return files.find((f) => f.path === selectedPath) || null
100
+ }, [files, selectedPath])
101
+
102
+ const selectedKind = previewKind(selectedPath || '')
103
+
104
+ useEffect(() => {
105
+ if (!selectedPath) {
106
+ setPreviewText('')
107
+ setPreviewError('')
108
+ setPreviewLoading(false)
109
+ return
110
+ }
111
+ if (selectedKind !== 'markdown' && selectedKind !== 'csv') {
112
+ setPreviewText('')
113
+ setPreviewError('')
114
+ setPreviewLoading(false)
115
+ return
116
+ }
117
+
118
+ const controller = new AbortController()
119
+ setPreviewLoading(true)
120
+ setPreviewError('')
121
+
122
+ void fetch(workspaceOutputContentUrl(selectedPath), { signal: controller.signal })
123
+ .then(async (response) => {
124
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
125
+ return response.text()
126
+ })
127
+ .then((text) => setPreviewText(text))
128
+ .catch((err) => {
129
+ if (controller.signal.aborted) return
130
+ setPreviewError(err instanceof Error ? err.message : String(err))
131
+ })
132
+ .finally(() => {
133
+ if (!controller.signal.aborted) setPreviewLoading(false)
134
+ })
135
+
136
+ return () => controller.abort()
137
+ }, [selectedPath, selectedKind])
138
+
139
+ const contentUrl = selectedPath ? workspaceOutputContentUrl(selectedPath) : ''
140
+ const downloadUrl = selectedPath ? workspaceOutputDownloadUrl(selectedPath) : ''
141
+
142
+ if (loading) {
143
+ return (
144
+ <div className="files-view">
145
+ <div className="files-empty"><div className="muted">Loading files...</div></div>
146
+ </div>
147
+ )
148
+ }
149
+
150
+ if (error) {
151
+ return (
152
+ <div className="files-view">
153
+ <div className="files-empty"><div className="error">Error: {error}</div></div>
154
+ </div>
155
+ )
156
+ }
157
+
158
+ if (files.length === 0) {
159
+ return (
160
+ <div className="files-view">
161
+ <div className="files-empty">
162
+ <div className="files-empty-inner">
163
+ <div className="files-empty-icon">
164
+ <FileType2 size={32} strokeWidth={1.4} />
165
+ </div>
166
+ <div className="files-empty-title">No output files yet</div>
167
+ <div className="files-empty-desc">Files generated by tasks will appear here.</div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ )
172
+ }
173
+
174
+ return (
175
+ <div className="files-view">
176
+ <div className="files-sidebar">
177
+ <div className="files-sidebar-header">
178
+ <span className="files-section-title">Output Files</span>
179
+ <span className="files-count">{files.length}</span>
180
+ </div>
181
+ <div className="files-list">
182
+ {files.map((file) => {
183
+ const Icon = fileIcon(file.path)
184
+ return (
185
+ <button
186
+ key={file.path}
187
+ className={`files-item ${selectedPath === file.path ? 'active' : ''}`}
188
+ onClick={() => setSelectedPath(file.path)}
189
+ title={file.path}
190
+ >
191
+ <span className="files-item-icon"><Icon size={15} strokeWidth={1.8} /></span>
192
+ <span className="files-item-name">{fileName(file.path)}</span>
193
+ <span className="files-item-size">{formatBytes(file.size)}</span>
194
+ </button>
195
+ )
196
+ })}
197
+ </div>
198
+ </div>
199
+ <div className="files-content">
200
+ {selectedFile && (
201
+ <>
202
+ <div className="files-preview-header">
203
+ <div className="files-preview-path">{selectedFile.path}</div>
204
+ <a className="output-download-btn" href={downloadUrl}>
205
+ Save to Downloads
206
+ </a>
207
+ </div>
208
+
209
+ {previewLoading && <div className="muted">Loading preview...</div>}
210
+ {previewError && <div className="error">Error: {previewError}</div>}
211
+
212
+ {!previewLoading && !previewError && selectedKind === 'markdown' && (
213
+ <div className="output-markdown-preview">
214
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{previewText}</ReactMarkdown>
215
+ </div>
216
+ )}
217
+
218
+ {!previewLoading && !previewError && selectedKind === 'html' && (
219
+ <iframe className="output-html-preview" src={contentUrl} title={selectedFile.path} />
220
+ )}
221
+
222
+ {!previewLoading && !previewError && selectedKind === 'image' && (
223
+ <div className="output-image-wrap">
224
+ <img src={contentUrl} alt={selectedFile.path} className="output-image-preview" />
225
+ </div>
226
+ )}
227
+
228
+ {!previewLoading && !previewError && selectedKind === 'csv' && (
229
+ <div className="output-csv-wrap">
230
+ <table className="output-csv-table">
231
+ <tbody>
232
+ {parseCsv(previewText).map((row, rowIndex) => (
233
+ <tr key={`row-${rowIndex}`}>
234
+ {row.map((cell, cellIndex) => (
235
+ <td key={`cell-${rowIndex}-${cellIndex}`}>{cell}</td>
236
+ ))}
237
+ </tr>
238
+ ))}
239
+ </tbody>
240
+ </table>
241
+ </div>
242
+ )}
243
+
244
+ {!previewLoading && !previewError && selectedKind === 'download' && (
245
+ <div className="output-fallback-preview">
246
+ <div className="muted">Preview is not supported for this file type.</div>
247
+ <a className="output-download-btn" href={downloadUrl}>
248
+ Save to Downloads
249
+ </a>
250
+ </div>
251
+ )}
252
+ </>
253
+ )}
254
+ </div>
255
+ </div>
256
+ )
257
+ }
@@ -0,0 +1,158 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+ import { Sparkles, Info, ChevronRight } from 'lucide-react'
3
+ import ReactMarkdown from 'react-markdown'
4
+ import remarkGfm from 'remark-gfm'
5
+ import { InputBar } from './InputBar'
6
+
7
+ function ToolGroup({ entry }) {
8
+ const [expanded, setExpanded] = useState(false)
9
+ return (
10
+ <div>
11
+ <div className="conv-tool-group" onClick={() => setExpanded((e) => !e)}>
12
+ <ChevronRight
13
+ size={12}
14
+ strokeWidth={2}
15
+ className={`conv-tool-group-chevron${expanded ? ' expanded' : ''}`}
16
+ />
17
+ <span className="conv-tool-group-summary">{entry.summary}</span>
18
+ </div>
19
+ {expanded && (
20
+ <div className="conv-tool-group-items">
21
+ {entry.tools.map((t) => (
22
+ <div key={t.id} className="conv-tool">
23
+ <span className="conv-tool-label">{t.label || t.name || 'Tool'}</span>
24
+ {t.summary ? <span className="conv-tool-summary">{t.summary}</span> : null}
25
+ </div>
26
+ ))}
27
+ </div>
28
+ )}
29
+ </div>
30
+ )
31
+ }
32
+
33
+ function renderEntry(entry) {
34
+ if (entry.type === 'tool_group') {
35
+ return <ToolGroup key={entry.id} entry={entry} />
36
+ }
37
+
38
+ if (entry.type === 'tool') {
39
+ return (
40
+ <div key={entry.id} className="conv-tool">
41
+ <span className="conv-tool-label">{entry.label || entry.name || 'Tool'}</span>
42
+ {entry.summary ? (
43
+ <span className="conv-tool-summary">{entry.summary}</span>
44
+ ) : null}
45
+ </div>
46
+ )
47
+ }
48
+
49
+ if (entry.type === 'status') {
50
+ return (
51
+ <div key={entry.id} className="conv-status">
52
+ <Info size={12} strokeWidth={2} />
53
+ <span>{entry.content}</span>
54
+ </div>
55
+ )
56
+ }
57
+
58
+ const isUser = entry.type === 'user'
59
+ const assistantName =
60
+ typeof entry.agentName === 'string' && entry.agentName.trim()
61
+ ? entry.agentName.trim()
62
+ : 'Assistant'
63
+ const assistantEmoji =
64
+ typeof entry.agentEmoji === 'string' && entry.agentEmoji.trim()
65
+ ? entry.agentEmoji.trim()
66
+ : ''
67
+
68
+ if (isUser) {
69
+ return (
70
+ <div key={entry.id} className="conv-msg conv-msg-user">
71
+ <div className="conv-msg-bubble">
72
+ <div className="conv-msg-text">{entry.content}</div>
73
+ </div>
74
+ </div>
75
+ )
76
+ }
77
+
78
+ return (
79
+ <div key={entry.id} className="conv-msg conv-msg-assistant">
80
+ <div className="conv-msg-avatar">
81
+ {assistantEmoji ? (
82
+ <span className="conv-msg-avatar-emoji">{assistantEmoji}</span>
83
+ ) : (
84
+ <Sparkles size={14} strokeWidth={2} />
85
+ )}
86
+ </div>
87
+ <div className="conv-msg-body">
88
+ <div className="conv-msg-role">{assistantName}</div>
89
+ <div className="conv-msg-markdown">
90
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{entry.content || ''}</ReactMarkdown>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ )
95
+ }
96
+
97
+ export function InlineChatView({
98
+ entries = [],
99
+ loading = false,
100
+ error = '',
101
+ onSubmit,
102
+ onStop,
103
+ onOpenOrchestratorConfig,
104
+ value,
105
+ onValueChange,
106
+ disabled = false,
107
+ }) {
108
+ const bodyRef = useRef(null)
109
+ const isNearBottomRef = useRef(true)
110
+
111
+ const handleMessagesScroll = useCallback(() => {
112
+ const el = bodyRef.current
113
+ if (!el) return
114
+ const threshold = 80
115
+ isNearBottomRef.current =
116
+ el.scrollHeight - el.scrollTop - el.clientHeight <= threshold
117
+ }, [])
118
+
119
+ useEffect(() => {
120
+ if (!bodyRef.current || !isNearBottomRef.current) {
121
+ return
122
+ }
123
+ bodyRef.current.scrollTop = bodyRef.current.scrollHeight
124
+ }, [entries, loading, error])
125
+
126
+ return (
127
+ <div className="inline-chat-view">
128
+ <div className="inline-chat-messages" ref={bodyRef} onScroll={handleMessagesScroll}>
129
+ {entries.length === 0 && (
130
+ <div className="muted" style={{ padding: '8px 0' }}>No messages yet.</div>
131
+ )}
132
+ {entries.map(renderEntry)}
133
+ {loading && (
134
+ <div className="conv-thinking-indicator">
135
+ <span className="conv-thinking-dot" />
136
+ <span className="conv-thinking-dot" />
137
+ <span className="conv-thinking-dot" />
138
+ </div>
139
+ )}
140
+ {error && (
141
+ <div className="error" style={{ padding: '4px 0' }}>Error: {error}</div>
142
+ )}
143
+ </div>
144
+ <InputBar
145
+ disabled={disabled}
146
+ loading={loading}
147
+ onStop={onStop}
148
+ onOpenOrchestratorConfig={onOpenOrchestratorConfig}
149
+ hideMode
150
+ lockedMode="chat"
151
+ embedded
152
+ value={value}
153
+ onValueChange={onValueChange}
154
+ onSubmit={onSubmit}
155
+ />
156
+ </div>
157
+ )
158
+ }