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,658 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { ChevronDown, ChevronRight, 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
|
+
fetchTaskOutputChanges,
|
|
8
|
+
fetchTaskOutputFiles,
|
|
9
|
+
taskOutputContentUrl,
|
|
10
|
+
taskOutputDownloadUrl,
|
|
11
|
+
} from '../api'
|
|
12
|
+
import { TestingPane } from './TestingPane'
|
|
13
|
+
|
|
14
|
+
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'])
|
|
15
|
+
|
|
16
|
+
function ext(path) {
|
|
17
|
+
const index = path.lastIndexOf('.')
|
|
18
|
+
if (index === -1) return ''
|
|
19
|
+
return path.slice(index).toLowerCase()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function previewKind(path) {
|
|
23
|
+
const extension = ext(path)
|
|
24
|
+
if (extension === '.md') return 'markdown'
|
|
25
|
+
if (extension === '.html' || extension === '.htm') return 'html'
|
|
26
|
+
if (extension === '.csv') return 'csv'
|
|
27
|
+
if (IMAGE_EXTENSIONS.has(extension)) return 'image'
|
|
28
|
+
return 'download'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fileName(path) {
|
|
32
|
+
const segments = String(path || '').split('/').filter(Boolean)
|
|
33
|
+
return segments[segments.length - 1] || path
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function outputIcon(path) {
|
|
37
|
+
const extension = ext(path)
|
|
38
|
+
if (extension === '.md' || extension === '.txt') return FileText
|
|
39
|
+
if (extension === '.html' || extension === '.htm' || extension === '.js' || extension === '.ts') return FileCode2
|
|
40
|
+
if (extension === '.csv') return FileSpreadsheet
|
|
41
|
+
if (IMAGE_EXTENSIONS.has(extension)) return FileImage
|
|
42
|
+
return FileType2
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatBytes(value) {
|
|
46
|
+
if (!Number.isFinite(value) || value < 0) return ''
|
|
47
|
+
if (value < 1024) return `${value} B`
|
|
48
|
+
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`
|
|
49
|
+
return `${(value / (1024 * 1024)).toFixed(1)} MB`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseCsv(text) {
|
|
53
|
+
const parsed = Papa.parse(text, {
|
|
54
|
+
skipEmptyLines: 'greedy',
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
if (parsed.errors.length > 0) {
|
|
58
|
+
throw new Error(parsed.errors[0].message || 'Failed to parse CSV.')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!Array.isArray(parsed.data)) {
|
|
62
|
+
return []
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return parsed.data.map((row) => (Array.isArray(row) ? row : [String(row)]))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseDiffIntoFiles(diff) {
|
|
69
|
+
if (!diff || !diff.trim()) return []
|
|
70
|
+
|
|
71
|
+
const lines = diff.split('\n')
|
|
72
|
+
const files = []
|
|
73
|
+
let current = null
|
|
74
|
+
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
if (line.startsWith('diff --git ')) {
|
|
77
|
+
const match = line.match(/^diff --git a\/.+ b\/(.+)$/)
|
|
78
|
+
current = {
|
|
79
|
+
path: match ? match[1] : 'unknown file',
|
|
80
|
+
additions: 0,
|
|
81
|
+
deletions: 0,
|
|
82
|
+
lines: [],
|
|
83
|
+
}
|
|
84
|
+
files.push(current)
|
|
85
|
+
current.lines.push(line)
|
|
86
|
+
} else if (current) {
|
|
87
|
+
current.lines.push(line)
|
|
88
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
89
|
+
current.additions++
|
|
90
|
+
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
91
|
+
current.deletions++
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fallback: if the diff has content but no `diff --git` markers, treat as one file
|
|
97
|
+
if (files.length === 0 && diff.trim()) {
|
|
98
|
+
let additions = 0
|
|
99
|
+
let deletions = 0
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
if (line.startsWith('+') && !line.startsWith('+++')) additions++
|
|
102
|
+
else if (line.startsWith('-') && !line.startsWith('---')) deletions++
|
|
103
|
+
}
|
|
104
|
+
files.push({ path: 'changes', additions, deletions, lines })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return files
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function FileDiff({ file, defaultOpen }) {
|
|
111
|
+
const [open, setOpen] = useState(defaultOpen)
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="diff-file">
|
|
115
|
+
<button className="diff-file-header" onClick={() => setOpen((v) => !v)}>
|
|
116
|
+
<span className="diff-file-chevron">
|
|
117
|
+
{open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
118
|
+
</span>
|
|
119
|
+
<span className="diff-file-path">{file.path}</span>
|
|
120
|
+
<span className="diff-file-stats">
|
|
121
|
+
{file.additions > 0 && <span className="diff-stat-add">+{file.additions}</span>}
|
|
122
|
+
{file.deletions > 0 && <span className="diff-stat-remove">-{file.deletions}</span>}
|
|
123
|
+
</span>
|
|
124
|
+
</button>
|
|
125
|
+
{open && (
|
|
126
|
+
<div className="diff-file-body">
|
|
127
|
+
{file.lines.map((line, index) => {
|
|
128
|
+
let className = 'diff-line'
|
|
129
|
+
if (line.startsWith('diff --git')) {
|
|
130
|
+
className += ' meta'
|
|
131
|
+
} else if (line.startsWith('+++') || line.startsWith('---')) {
|
|
132
|
+
className += ' header'
|
|
133
|
+
} else if (line.startsWith('@@')) {
|
|
134
|
+
className += ' hunk'
|
|
135
|
+
} else if (line.startsWith('+')) {
|
|
136
|
+
className += ' add'
|
|
137
|
+
} else if (line.startsWith('-')) {
|
|
138
|
+
className += ' remove'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div key={`line-${index}`} className={className}>
|
|
143
|
+
{line || ' '}
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
})}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function DiffViewer({ diff }) {
|
|
154
|
+
const files = useMemo(() => parseDiffIntoFiles(diff), [diff])
|
|
155
|
+
|
|
156
|
+
if (files.length === 0) {
|
|
157
|
+
return <div className="muted">No changes detected.</div>
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div className="diff-viewer">
|
|
162
|
+
<div className="diff-summary">
|
|
163
|
+
{files.length} {files.length === 1 ? 'file' : 'files'} changed
|
|
164
|
+
</div>
|
|
165
|
+
{files.map((file, index) => (
|
|
166
|
+
<FileDiff key={`${file.path}-${index}`} file={file} defaultOpen={files.length <= 5} />
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function formatRuntimeLogEntry(entry) {
|
|
173
|
+
if (!entry || typeof entry !== 'object') {
|
|
174
|
+
return ''
|
|
175
|
+
}
|
|
176
|
+
const stream = typeof entry.stream === 'string' ? entry.stream : 'log'
|
|
177
|
+
const text = typeof entry.text === 'string' ? entry.text : ''
|
|
178
|
+
return `[${stream}] ${text}`
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function OutputPanel({
|
|
182
|
+
taskId,
|
|
183
|
+
taskStatus,
|
|
184
|
+
testCommands = [],
|
|
185
|
+
testCommandsLoading = false,
|
|
186
|
+
testCommandsError = '',
|
|
187
|
+
runningTestCommand = '',
|
|
188
|
+
onRunTestCommand,
|
|
189
|
+
onUpdateTestCommand,
|
|
190
|
+
onChangesLoaded,
|
|
191
|
+
onFilesLoaded,
|
|
192
|
+
planData = null,
|
|
193
|
+
planLoading = false,
|
|
194
|
+
planError = '',
|
|
195
|
+
runtimeService = null,
|
|
196
|
+
runtimeServiceLogs = [],
|
|
197
|
+
runtimeServiceError = '',
|
|
198
|
+
onStopRuntimeService,
|
|
199
|
+
testingConfig = null,
|
|
200
|
+
}) {
|
|
201
|
+
const [tab, setTab] = useState('outputs')
|
|
202
|
+
const runtimeLogRef = useRef(null)
|
|
203
|
+
const [commandDrafts, setCommandDrafts] = useState({})
|
|
204
|
+
const [savingCommandIndex, setSavingCommandIndex] = useState(-1)
|
|
205
|
+
const [editingCommandIndex, setEditingCommandIndex] = useState(-1)
|
|
206
|
+
|
|
207
|
+
const [changes, setChanges] = useState('')
|
|
208
|
+
const [changesLoading, setChangesLoading] = useState(false)
|
|
209
|
+
const [changesError, setChangesError] = useState('')
|
|
210
|
+
|
|
211
|
+
const [files, setFiles] = useState([])
|
|
212
|
+
const [filesLoading, setFilesLoading] = useState(false)
|
|
213
|
+
const [filesError, setFilesError] = useState('')
|
|
214
|
+
|
|
215
|
+
const [selectedPath, setSelectedPath] = useState('')
|
|
216
|
+
const [previewText, setPreviewText] = useState('')
|
|
217
|
+
const [previewLoading, setPreviewLoading] = useState(false)
|
|
218
|
+
const [previewError, setPreviewError] = useState('')
|
|
219
|
+
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (!taskId) return
|
|
222
|
+
setTab(testingConfig ? 'testing' : 'outputs')
|
|
223
|
+
void loadChanges(taskId)
|
|
224
|
+
void loadFiles(taskId)
|
|
225
|
+
}, [taskId, testingConfig])
|
|
226
|
+
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (!files.length) {
|
|
229
|
+
setSelectedPath('')
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
setSelectedPath((current) => {
|
|
234
|
+
if (current && files.some((item) => item.path === current)) {
|
|
235
|
+
return current
|
|
236
|
+
}
|
|
237
|
+
return files[0].path
|
|
238
|
+
})
|
|
239
|
+
}, [files])
|
|
240
|
+
|
|
241
|
+
const selectedFile = useMemo(() => {
|
|
242
|
+
return files.find((file) => file.path === selectedPath) || null
|
|
243
|
+
}, [files, selectedPath])
|
|
244
|
+
|
|
245
|
+
const selectedKind = previewKind(selectedPath || '')
|
|
246
|
+
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (!taskId || !selectedPath) {
|
|
249
|
+
setPreviewText('')
|
|
250
|
+
setPreviewError('')
|
|
251
|
+
setPreviewLoading(false)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (selectedKind !== 'markdown' && selectedKind !== 'csv') {
|
|
256
|
+
setPreviewText('')
|
|
257
|
+
setPreviewError('')
|
|
258
|
+
setPreviewLoading(false)
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const controller = new AbortController()
|
|
263
|
+
setPreviewLoading(true)
|
|
264
|
+
setPreviewError('')
|
|
265
|
+
|
|
266
|
+
void fetch(taskOutputContentUrl(taskId, selectedPath), { signal: controller.signal })
|
|
267
|
+
.then(async (response) => {
|
|
268
|
+
if (!response.ok) {
|
|
269
|
+
throw new Error(`HTTP ${response.status}`)
|
|
270
|
+
}
|
|
271
|
+
return response.text()
|
|
272
|
+
})
|
|
273
|
+
.then((text) => {
|
|
274
|
+
setPreviewText(text)
|
|
275
|
+
})
|
|
276
|
+
.catch((error) => {
|
|
277
|
+
if (controller.signal.aborted) {
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
setPreviewError(error instanceof Error ? error.message : String(error))
|
|
281
|
+
})
|
|
282
|
+
.finally(() => {
|
|
283
|
+
if (!controller.signal.aborted) {
|
|
284
|
+
setPreviewLoading(false)
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
return () => {
|
|
289
|
+
controller.abort()
|
|
290
|
+
}
|
|
291
|
+
}, [taskId, selectedPath, selectedKind])
|
|
292
|
+
|
|
293
|
+
async function loadChanges(targetTaskId) {
|
|
294
|
+
setChangesLoading(true)
|
|
295
|
+
setChangesError('')
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const data = await fetchTaskOutputChanges(targetTaskId)
|
|
299
|
+
const diff = typeof data.diff === 'string' ? data.diff : ''
|
|
300
|
+
setChanges(diff)
|
|
301
|
+
onChangesLoaded?.(Boolean(diff.trim()))
|
|
302
|
+
} catch (error) {
|
|
303
|
+
setChangesError(error instanceof Error ? error.message : String(error))
|
|
304
|
+
setChanges('')
|
|
305
|
+
onChangesLoaded?.(false)
|
|
306
|
+
} finally {
|
|
307
|
+
setChangesLoading(false)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function loadFiles(targetTaskId) {
|
|
312
|
+
setFilesLoading(true)
|
|
313
|
+
setFilesError('')
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const data = await fetchTaskOutputFiles(targetTaskId)
|
|
317
|
+
const nextFiles = Array.isArray(data.files) ? data.files : []
|
|
318
|
+
setFiles(nextFiles)
|
|
319
|
+
onFilesLoaded?.(nextFiles.length > 0)
|
|
320
|
+
} catch (error) {
|
|
321
|
+
setFilesError(error instanceof Error ? error.message : String(error))
|
|
322
|
+
setFiles([])
|
|
323
|
+
onFilesLoaded?.(false)
|
|
324
|
+
} finally {
|
|
325
|
+
setFilesLoading(false)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const outputDownloadUrl = selectedPath ? taskOutputDownloadUrl(taskId, selectedPath) : ''
|
|
330
|
+
const outputContentUrl = selectedPath ? taskOutputContentUrl(taskId, selectedPath) : ''
|
|
331
|
+
const runtimeLogText = useMemo(() => {
|
|
332
|
+
if (!Array.isArray(runtimeServiceLogs) || runtimeServiceLogs.length === 0) {
|
|
333
|
+
return ''
|
|
334
|
+
}
|
|
335
|
+
return runtimeServiceLogs.map((entry) => formatRuntimeLogEntry(entry)).join('')
|
|
336
|
+
}, [runtimeServiceLogs])
|
|
337
|
+
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
if (!runtimeLogRef.current) {
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
runtimeLogRef.current.scrollTop = runtimeLogRef.current.scrollHeight
|
|
343
|
+
}, [runtimeLogText])
|
|
344
|
+
|
|
345
|
+
useEffect(() => {
|
|
346
|
+
const next = {}
|
|
347
|
+
testCommands.forEach((item, index) => {
|
|
348
|
+
next[index] = typeof item?.command === 'string' ? item.command : ''
|
|
349
|
+
})
|
|
350
|
+
setCommandDrafts(next)
|
|
351
|
+
}, [testCommands])
|
|
352
|
+
|
|
353
|
+
function commandDraft(index, fallback) {
|
|
354
|
+
const value = commandDrafts[index]
|
|
355
|
+
if (typeof value === 'string') {
|
|
356
|
+
return value
|
|
357
|
+
}
|
|
358
|
+
return fallback
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function commitCommandEdit(index, fallbackCommand) {
|
|
362
|
+
const current = typeof fallbackCommand === 'string' ? fallbackCommand.trim() : ''
|
|
363
|
+
const nextValue = commandDraft(index, fallbackCommand).trim()
|
|
364
|
+
if (!nextValue || nextValue === current) {
|
|
365
|
+
setCommandDrafts((prev) => ({
|
|
366
|
+
...prev,
|
|
367
|
+
[index]: current,
|
|
368
|
+
}))
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
setSavingCommandIndex(index)
|
|
373
|
+
try {
|
|
374
|
+
await onUpdateTestCommand?.(index, nextValue)
|
|
375
|
+
setEditingCommandIndex((current) => (current === index ? -1 : current))
|
|
376
|
+
} finally {
|
|
377
|
+
setSavingCommandIndex(-1)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<div className="output-panel">
|
|
383
|
+
<div className="output-tabs">
|
|
384
|
+
<button
|
|
385
|
+
className={`output-tab-btn ${tab === 'outputs' ? 'active' : ''}`}
|
|
386
|
+
onClick={() => setTab('outputs')}
|
|
387
|
+
>
|
|
388
|
+
Outputs
|
|
389
|
+
</button>
|
|
390
|
+
<button
|
|
391
|
+
className={`output-tab-btn ${tab === 'changes' ? 'active' : ''}`}
|
|
392
|
+
onClick={() => setTab('changes')}
|
|
393
|
+
>
|
|
394
|
+
Changes
|
|
395
|
+
</button>
|
|
396
|
+
<button
|
|
397
|
+
className={`output-tab-btn ${tab === 'plan' ? 'active' : ''}`}
|
|
398
|
+
onClick={() => setTab('plan')}
|
|
399
|
+
>
|
|
400
|
+
Plan
|
|
401
|
+
</button>
|
|
402
|
+
{testingConfig && (
|
|
403
|
+
<button
|
|
404
|
+
className={`output-tab-btn ${tab === 'testing' ? 'active' : ''}`}
|
|
405
|
+
onClick={() => setTab('testing')}
|
|
406
|
+
>
|
|
407
|
+
Testing
|
|
408
|
+
</button>
|
|
409
|
+
)}
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
{tab === 'changes' && (
|
|
413
|
+
<div className="output-tab-body">
|
|
414
|
+
{changesLoading && <div className="muted">Loading diff...</div>}
|
|
415
|
+
{changesError && <div className="error">Error: {changesError}</div>}
|
|
416
|
+
{!changesLoading && !changesError && <DiffViewer diff={changes} />}
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
|
|
420
|
+
{tab === 'outputs' && (
|
|
421
|
+
<div className="output-tab-body output-outputs-layout">
|
|
422
|
+
{testCommandsLoading && <div className="muted">Loading test commands...</div>}
|
|
423
|
+
{!testCommandsLoading && testCommands.length > 0 && (
|
|
424
|
+
<div className="output-run-card">
|
|
425
|
+
<div className="output-run-list">
|
|
426
|
+
{testCommands.map((item, index) => {
|
|
427
|
+
const command = typeof item?.command === 'string' ? item.command : ''
|
|
428
|
+
const cwd = typeof item?.cwd === 'string' ? item.cwd : ''
|
|
429
|
+
const runKey = `${cwd}\n${command}`
|
|
430
|
+
const label = typeof item?.label === 'string' && item.label.trim()
|
|
431
|
+
? item.label.trim()
|
|
432
|
+
: command || `Test ${index + 1}`
|
|
433
|
+
if (!command || !cwd) {
|
|
434
|
+
return null
|
|
435
|
+
}
|
|
436
|
+
return (
|
|
437
|
+
<div key={`${runKey}-${index}`} className="output-run-item">
|
|
438
|
+
<button
|
|
439
|
+
className="output-run-btn"
|
|
440
|
+
title={`${command}\nCWD: ${cwd}`}
|
|
441
|
+
onClick={() => {
|
|
442
|
+
void onRunTestCommand?.({
|
|
443
|
+
...item,
|
|
444
|
+
command,
|
|
445
|
+
})
|
|
446
|
+
}}
|
|
447
|
+
disabled={
|
|
448
|
+
Boolean(runningTestCommand) ||
|
|
449
|
+
savingCommandIndex === index ||
|
|
450
|
+
editingCommandIndex === index
|
|
451
|
+
}
|
|
452
|
+
>
|
|
453
|
+
{runningTestCommand === runKey ? `Running: ${label}...` : `Run: ${label}`}
|
|
454
|
+
</button>
|
|
455
|
+
{editingCommandIndex === index ? (
|
|
456
|
+
<>
|
|
457
|
+
<input
|
|
458
|
+
className="output-run-command-input"
|
|
459
|
+
value={commandDraft(index, command)}
|
|
460
|
+
title={`${command}\nCWD: ${cwd}`}
|
|
461
|
+
onChange={(event) => {
|
|
462
|
+
const value = event.target.value
|
|
463
|
+
setCommandDrafts((prev) => ({
|
|
464
|
+
...prev,
|
|
465
|
+
[index]: value,
|
|
466
|
+
}))
|
|
467
|
+
}}
|
|
468
|
+
onKeyDown={(event) => {
|
|
469
|
+
if (event.key === 'Escape') {
|
|
470
|
+
event.preventDefault()
|
|
471
|
+
setCommandDrafts((prev) => ({
|
|
472
|
+
...prev,
|
|
473
|
+
[index]: command,
|
|
474
|
+
}))
|
|
475
|
+
setEditingCommandIndex(-1)
|
|
476
|
+
}
|
|
477
|
+
}}
|
|
478
|
+
disabled={Boolean(runningTestCommand) || savingCommandIndex === index}
|
|
479
|
+
/>
|
|
480
|
+
<button
|
|
481
|
+
className="output-run-save-btn"
|
|
482
|
+
onClick={() => {
|
|
483
|
+
void commitCommandEdit(index, command)
|
|
484
|
+
}}
|
|
485
|
+
disabled={Boolean(runningTestCommand) || savingCommandIndex === index}
|
|
486
|
+
title="Save command"
|
|
487
|
+
>
|
|
488
|
+
✓
|
|
489
|
+
</button>
|
|
490
|
+
</>
|
|
491
|
+
) : (
|
|
492
|
+
<>
|
|
493
|
+
<span className="output-run-command" title={`${command}\nCWD: ${cwd}`}>
|
|
494
|
+
{command}
|
|
495
|
+
</span>
|
|
496
|
+
<button
|
|
497
|
+
className="output-run-edit-btn"
|
|
498
|
+
onClick={() => {
|
|
499
|
+
setCommandDrafts((prev) => ({
|
|
500
|
+
...prev,
|
|
501
|
+
[index]: command,
|
|
502
|
+
}))
|
|
503
|
+
setEditingCommandIndex(index)
|
|
504
|
+
}}
|
|
505
|
+
disabled={Boolean(runningTestCommand) || savingCommandIndex === index}
|
|
506
|
+
title="Edit command"
|
|
507
|
+
>
|
|
508
|
+
Edit
|
|
509
|
+
</button>
|
|
510
|
+
</>
|
|
511
|
+
)}
|
|
512
|
+
</div>
|
|
513
|
+
)
|
|
514
|
+
})}
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
)}
|
|
518
|
+
{testCommandsError && <div className="error">Error: {testCommandsError}</div>}
|
|
519
|
+
|
|
520
|
+
{runtimeService && runtimeService.task_id === taskId && (
|
|
521
|
+
<div className="output-runtime-card">
|
|
522
|
+
<div className="output-runtime-header">
|
|
523
|
+
<div className="output-runtime-title-wrap">
|
|
524
|
+
<div className="output-runtime-title">{runtimeService.label || runtimeService.command}</div>
|
|
525
|
+
<div className="output-runtime-meta">
|
|
526
|
+
{runtimeService.status} · {runtimeService.cwd}
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
{runtimeService.status === 'running' && (
|
|
530
|
+
<button
|
|
531
|
+
className="output-runtime-stop"
|
|
532
|
+
onClick={() => onStopRuntimeService?.(runtimeService.id)}
|
|
533
|
+
>
|
|
534
|
+
Stop
|
|
535
|
+
</button>
|
|
536
|
+
)}
|
|
537
|
+
</div>
|
|
538
|
+
{runtimeServiceError && <div className="error">Error: {runtimeServiceError}</div>}
|
|
539
|
+
<pre className="output-runtime-log" ref={runtimeLogRef}>
|
|
540
|
+
{runtimeLogText || 'No terminal output yet.'}
|
|
541
|
+
</pre>
|
|
542
|
+
</div>
|
|
543
|
+
)}
|
|
544
|
+
|
|
545
|
+
{filesLoading && <div className="muted">Loading files...</div>}
|
|
546
|
+
{filesError && <div className="error">Error: {filesError}</div>}
|
|
547
|
+
|
|
548
|
+
{!filesLoading && !filesError && files.length > 0 && (
|
|
549
|
+
<>
|
|
550
|
+
<div className="output-file-grid">
|
|
551
|
+
{files.map((file) => {
|
|
552
|
+
const Icon = outputIcon(file.path)
|
|
553
|
+
return (
|
|
554
|
+
<button
|
|
555
|
+
key={file.path}
|
|
556
|
+
className={`output-file-tile ${selectedPath === file.path ? 'active' : ''}`}
|
|
557
|
+
onClick={() => setSelectedPath(file.path)}
|
|
558
|
+
title={file.path}
|
|
559
|
+
>
|
|
560
|
+
<span className="output-file-tile-icon">
|
|
561
|
+
<Icon size={24} strokeWidth={1.9} />
|
|
562
|
+
</span>
|
|
563
|
+
<span className="output-file-tile-name">{fileName(file.path)}</span>
|
|
564
|
+
<span className="output-file-tile-size">{formatBytes(file.size)}</span>
|
|
565
|
+
</button>
|
|
566
|
+
)
|
|
567
|
+
})}
|
|
568
|
+
</div>
|
|
569
|
+
|
|
570
|
+
{selectedFile && (
|
|
571
|
+
<div className="output-preview-card">
|
|
572
|
+
<div className="output-preview-header">
|
|
573
|
+
<div className="output-preview-path">{selectedFile.path}</div>
|
|
574
|
+
<div className="output-preview-actions">
|
|
575
|
+
<a className="output-download-btn" href={outputDownloadUrl}>
|
|
576
|
+
Save to Downloads
|
|
577
|
+
</a>
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
|
|
581
|
+
{previewLoading && <div className="muted">Loading preview...</div>}
|
|
582
|
+
{previewError && <div className="error">Error: {previewError}</div>}
|
|
583
|
+
|
|
584
|
+
{!previewLoading && !previewError && selectedKind === 'markdown' && (
|
|
585
|
+
<div className="output-markdown-preview">
|
|
586
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{previewText}</ReactMarkdown>
|
|
587
|
+
</div>
|
|
588
|
+
)}
|
|
589
|
+
|
|
590
|
+
{!previewLoading && !previewError && selectedKind === 'html' && (
|
|
591
|
+
<iframe className="output-html-preview" src={outputContentUrl} title={selectedFile.path} />
|
|
592
|
+
)}
|
|
593
|
+
|
|
594
|
+
{!previewLoading && !previewError && selectedKind === 'image' && (
|
|
595
|
+
<div className="output-image-wrap">
|
|
596
|
+
<img src={outputContentUrl} alt={selectedFile.path} className="output-image-preview" />
|
|
597
|
+
</div>
|
|
598
|
+
)}
|
|
599
|
+
|
|
600
|
+
{!previewLoading && !previewError && selectedKind === 'csv' && (
|
|
601
|
+
<div className="output-csv-wrap">
|
|
602
|
+
<table className="output-csv-table">
|
|
603
|
+
<tbody>
|
|
604
|
+
{parseCsv(previewText).map((row, rowIndex) => (
|
|
605
|
+
<tr key={`row-${rowIndex}`}>
|
|
606
|
+
{row.map((cell, cellIndex) => (
|
|
607
|
+
<td key={`cell-${rowIndex}-${cellIndex}`}>{cell}</td>
|
|
608
|
+
))}
|
|
609
|
+
</tr>
|
|
610
|
+
))}
|
|
611
|
+
</tbody>
|
|
612
|
+
</table>
|
|
613
|
+
</div>
|
|
614
|
+
)}
|
|
615
|
+
|
|
616
|
+
{!previewLoading && !previewError && selectedKind === 'download' && (
|
|
617
|
+
<div className="output-fallback-preview">
|
|
618
|
+
<div className="muted">Preview is not supported for this file type.</div>
|
|
619
|
+
<a className="output-download-btn" href={outputDownloadUrl}>
|
|
620
|
+
Save to Downloads
|
|
621
|
+
</a>
|
|
622
|
+
</div>
|
|
623
|
+
)}
|
|
624
|
+
</div>
|
|
625
|
+
)}
|
|
626
|
+
</>
|
|
627
|
+
)}
|
|
628
|
+
|
|
629
|
+
{!testCommandsLoading && testCommands.length === 0 && !filesLoading && files.length === 0 &&
|
|
630
|
+
!(runtimeService && runtimeService.task_id === taskId) && (
|
|
631
|
+
<div className="muted">No outputs yet.</div>
|
|
632
|
+
)}
|
|
633
|
+
</div>
|
|
634
|
+
)}
|
|
635
|
+
|
|
636
|
+
{tab === 'plan' && (
|
|
637
|
+
<div className="output-tab-body output-plan-body">
|
|
638
|
+
{planLoading && <div className="muted">Loading plan...</div>}
|
|
639
|
+
{planError && <div className="error">Error: {planError}</div>}
|
|
640
|
+
{!planLoading && !planError && planData && (
|
|
641
|
+
<div className="output-plan-markdown">
|
|
642
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{planData}</ReactMarkdown>
|
|
643
|
+
</div>
|
|
644
|
+
)}
|
|
645
|
+
{!planLoading && !planError && !planData && (
|
|
646
|
+
<div className="muted">No plan yet.</div>
|
|
647
|
+
)}
|
|
648
|
+
</div>
|
|
649
|
+
)}
|
|
650
|
+
|
|
651
|
+
{tab === 'testing' && testingConfig && (
|
|
652
|
+
<div className="output-tab-body output-testing-body">
|
|
653
|
+
<TestingPane taskId={taskId} config={testingConfig} />
|
|
654
|
+
</div>
|
|
655
|
+
)}
|
|
656
|
+
</div>
|
|
657
|
+
)
|
|
658
|
+
}
|