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,575 @@
|
|
|
1
|
+
import { iconForAgent } from './agentIcon'
|
|
2
|
+
|
|
3
|
+
export const ORCHESTRATOR_AGENT_ID = 'pragma-orchestrator'
|
|
4
|
+
|
|
5
|
+
export function getPendingCount(tasks) {
|
|
6
|
+
return tasks.filter((task) => {
|
|
7
|
+
const status = String(task.status).toLowerCase()
|
|
8
|
+
return (
|
|
9
|
+
status === 'pending_review' ||
|
|
10
|
+
status === 'waiting_for_recipient' ||
|
|
11
|
+
status === 'waiting_for_question_response' ||
|
|
12
|
+
status === 'waiting_for_help_response'
|
|
13
|
+
)
|
|
14
|
+
}).length
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isWaitingForHumanResponse(status) {
|
|
18
|
+
return status === 'waiting_for_question_response' || status === 'waiting_for_help_response' || status === 'pending_review'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isTaskActivelyRunning(status) {
|
|
22
|
+
const normalized = typeof status === 'string' ? status.trim().toLowerCase() : ''
|
|
23
|
+
return normalized === 'running' || normalized === 'orchestrating' || normalized === 'queued' || normalized === 'planning'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function hasRunningTurn(turns) {
|
|
27
|
+
if (!Array.isArray(turns)) return false
|
|
28
|
+
return turns.some((t) => t && typeof t.status === 'string' && t.status === 'running')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function errorText(error) {
|
|
32
|
+
return error instanceof Error ? error.message : String(error)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function nextEntryId(prefix) {
|
|
36
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const EXPLORE_LABELS = new Set([
|
|
40
|
+
'Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch',
|
|
41
|
+
'read', 'glob', 'grep', 'websearch', 'webfetch',
|
|
42
|
+
'search', 'list_directory',
|
|
43
|
+
])
|
|
44
|
+
const WRITE_LABELS = new Set([
|
|
45
|
+
'Write', 'Edit', 'NotebookEdit',
|
|
46
|
+
'write', 'edit', 'notebookedit', 'write_file',
|
|
47
|
+
])
|
|
48
|
+
const COMMAND_LABELS = new Set(['Bash', 'bash', 'shell'])
|
|
49
|
+
|
|
50
|
+
export function buildToolGroupSummary(tools) {
|
|
51
|
+
let explores = 0
|
|
52
|
+
let writes = 0
|
|
53
|
+
let commands = 0
|
|
54
|
+
let other = 0
|
|
55
|
+
const writtenFiles = new Set()
|
|
56
|
+
|
|
57
|
+
for (const t of tools) {
|
|
58
|
+
const lbl = t.label || ''
|
|
59
|
+
if (EXPLORE_LABELS.has(lbl)) {
|
|
60
|
+
explores++
|
|
61
|
+
} else if (WRITE_LABELS.has(lbl)) {
|
|
62
|
+
writes++
|
|
63
|
+
if (t.summary) writtenFiles.add(t.summary)
|
|
64
|
+
} else if (COMMAND_LABELS.has(lbl)) {
|
|
65
|
+
commands++
|
|
66
|
+
} else {
|
|
67
|
+
other++
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const parts = []
|
|
72
|
+
if (explores > 0) parts.push(`Explored ${explores} file${explores > 1 ? 's' : ''}`)
|
|
73
|
+
if (writes > 0) {
|
|
74
|
+
if (writtenFiles.size <= 3 && writtenFiles.size > 0) {
|
|
75
|
+
parts.push(`Updated ${[...writtenFiles].join(', ')}`)
|
|
76
|
+
} else {
|
|
77
|
+
parts.push(`Updated ${writes} file${writes > 1 ? 's' : ''}`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (commands > 0) parts.push(`Ran ${commands} command${commands > 1 ? 's' : ''}`)
|
|
81
|
+
if (other > 0 && parts.length === 0) parts.push(`Performed ${other} tool call${other > 1 ? 's' : ''}`)
|
|
82
|
+
|
|
83
|
+
return parts.length > 0 ? parts.join(', ') : `Performed ${tools.length} tool calls`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function groupConsecutiveToolEntries(entries) {
|
|
87
|
+
const result = []
|
|
88
|
+
let i = 0
|
|
89
|
+
while (i < entries.length) {
|
|
90
|
+
if (entries[i].type !== 'tool') {
|
|
91
|
+
result.push(entries[i])
|
|
92
|
+
i++
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
// Start of a tool run
|
|
96
|
+
const runStart = i
|
|
97
|
+
while (i < entries.length && entries[i].type === 'tool') {
|
|
98
|
+
i++
|
|
99
|
+
}
|
|
100
|
+
const run = entries.slice(runStart, i)
|
|
101
|
+
result.push({
|
|
102
|
+
id: nextEntryId('tool_group'),
|
|
103
|
+
type: 'tool_group',
|
|
104
|
+
tools: run,
|
|
105
|
+
summary: buildToolGroupSummary(run),
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
return result
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function appendToolEntryStreaming(entries, toolEntry) {
|
|
112
|
+
const last = entries[entries.length - 1]
|
|
113
|
+
if (last && last.type === 'tool_group') {
|
|
114
|
+
const tools = [...last.tools, toolEntry]
|
|
115
|
+
const updated = { ...last, tools, summary: buildToolGroupSummary(tools) }
|
|
116
|
+
return [...entries.slice(0, -1), updated]
|
|
117
|
+
}
|
|
118
|
+
if (last && last.type === 'tool') {
|
|
119
|
+
const tools = [last, toolEntry]
|
|
120
|
+
const group = {
|
|
121
|
+
id: nextEntryId('tool_group'),
|
|
122
|
+
type: 'tool_group',
|
|
123
|
+
tools,
|
|
124
|
+
summary: buildToolGroupSummary(tools),
|
|
125
|
+
}
|
|
126
|
+
return [...entries.slice(0, -1), group]
|
|
127
|
+
}
|
|
128
|
+
// Wrap single tool in a group immediately so it shows summary like "Ran 1 command"
|
|
129
|
+
const singleGroup = {
|
|
130
|
+
id: nextEntryId('tool_group'),
|
|
131
|
+
type: 'tool_group',
|
|
132
|
+
tools: [toolEntry],
|
|
133
|
+
summary: buildToolGroupSummary([toolEntry]),
|
|
134
|
+
}
|
|
135
|
+
return [...entries, singleGroup]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function appendAssistantDelta(entries, delta, assistantIdentity) {
|
|
139
|
+
if (!delta) {
|
|
140
|
+
return entries
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const last = entries[entries.length - 1]
|
|
144
|
+
if (last && last.type === 'assistant') {
|
|
145
|
+
const nextLast = {
|
|
146
|
+
...last,
|
|
147
|
+
content: last.content ? `${last.content}\n${delta}` : delta,
|
|
148
|
+
}
|
|
149
|
+
return [...entries.slice(0, -1), nextLast]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return [
|
|
153
|
+
...entries,
|
|
154
|
+
{
|
|
155
|
+
id: nextEntryId('assistant'),
|
|
156
|
+
type: 'assistant',
|
|
157
|
+
content: delta,
|
|
158
|
+
agentId: assistantIdentity?.agentId || '',
|
|
159
|
+
agentName: assistantIdentity?.agentName || '',
|
|
160
|
+
agentEmoji: assistantIdentity?.agentEmoji || '',
|
|
161
|
+
},
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function summarizeToolEvent(name, payload) {
|
|
166
|
+
if (!payload || typeof payload !== 'object') {
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (name === 'assistant.tool_use' && payload.type === 'tool_use') {
|
|
171
|
+
const toolName = typeof payload.name === 'string' ? payload.name : 'Tool'
|
|
172
|
+
const input = payload.input && typeof payload.input === 'object' ? payload.input : {}
|
|
173
|
+
return {
|
|
174
|
+
label: toolName,
|
|
175
|
+
summary: summarizeToolInput(input),
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (name.startsWith('item.')) {
|
|
180
|
+
if (name === 'item.reasoning' || name === 'item.plan') {
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const type = typeof payload.type === 'string' ? payload.type : name.replace('item.', '')
|
|
185
|
+
if (typeof payload.command === 'string') {
|
|
186
|
+
return { label: type, summary: truncate(payload.command, 140) }
|
|
187
|
+
}
|
|
188
|
+
if (typeof payload.file_path === 'string') {
|
|
189
|
+
return { label: type, summary: basename(payload.file_path) }
|
|
190
|
+
}
|
|
191
|
+
if (typeof payload.name === 'string') {
|
|
192
|
+
return { label: type, summary: payload.name }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function summarizeToolInput(input) {
|
|
200
|
+
if (typeof input.command === 'string' && input.command.trim()) {
|
|
201
|
+
return truncate(input.command.trim(), 140)
|
|
202
|
+
}
|
|
203
|
+
if (typeof input.file_path === 'string' && input.file_path.trim()) {
|
|
204
|
+
return basename(input.file_path.trim())
|
|
205
|
+
}
|
|
206
|
+
if (Array.isArray(input.paths) && input.paths.length > 0) {
|
|
207
|
+
return input.paths.map(basename).join(', ')
|
|
208
|
+
}
|
|
209
|
+
if (typeof input.description === 'string' && input.description.trim()) {
|
|
210
|
+
return truncate(input.description.trim(), 140)
|
|
211
|
+
}
|
|
212
|
+
if (typeof input.query === 'string' && input.query.trim()) {
|
|
213
|
+
return input.query.trim()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const keys = Object.keys(input)
|
|
217
|
+
if (keys.length > 0) {
|
|
218
|
+
return keys.slice(0, 4).join(', ')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return ''
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function truncate(value, maxLength) {
|
|
225
|
+
if (value.length <= maxLength) {
|
|
226
|
+
return value
|
|
227
|
+
}
|
|
228
|
+
return `${value.slice(0, maxLength - 3)}...`
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function basename(filePath) {
|
|
232
|
+
if (typeof filePath !== 'string') return filePath
|
|
233
|
+
const lastSlash = filePath.lastIndexOf('/')
|
|
234
|
+
if (lastSlash === -1) return filePath
|
|
235
|
+
return filePath.slice(lastSlash + 1) || filePath
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function normalizeTaskTitle(value) {
|
|
239
|
+
const title = typeof value === 'string' ? value.trim() : ''
|
|
240
|
+
if (!title) {
|
|
241
|
+
return ''
|
|
242
|
+
}
|
|
243
|
+
return title.replace(/^execute:\s*/i, '')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function resolveConversationHeaderAgent({ conversation, tasks, agentById }) {
|
|
247
|
+
const currentConversation = conversation && typeof conversation === 'object' ? conversation : null
|
|
248
|
+
const allTasks = Array.isArray(tasks) ? tasks : []
|
|
249
|
+
const entries = Array.isArray(currentConversation?.entries) ? currentConversation.entries : []
|
|
250
|
+
|
|
251
|
+
let resolvedAgentId = ''
|
|
252
|
+
if (typeof currentConversation?.taskId === 'string' && currentConversation.taskId) {
|
|
253
|
+
const currentTask = allTasks.find((task) => task?.id === currentConversation.taskId)
|
|
254
|
+
if (currentTask && typeof currentTask.assigned_to === 'string' && currentTask.assigned_to.trim()) {
|
|
255
|
+
resolvedAgentId = currentTask.assigned_to.trim()
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
260
|
+
const entry = entries[index]
|
|
261
|
+
if (!entry || entry.type !== 'assistant') {
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const entryAgentId =
|
|
266
|
+
typeof entry.agentId === 'string' && entry.agentId.trim() ? entry.agentId.trim() : ''
|
|
267
|
+
if (entryAgentId) {
|
|
268
|
+
resolvedAgentId = entryAgentId
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const entryAgentName =
|
|
273
|
+
typeof entry.agentName === 'string' && entry.agentName.trim() ? entry.agentName.trim() : ''
|
|
274
|
+
const entryAgentEmoji =
|
|
275
|
+
typeof entry.agentEmoji === 'string' && entry.agentEmoji.trim() ? entry.agentEmoji.trim() : ''
|
|
276
|
+
if (entryAgentName || entryAgentEmoji) {
|
|
277
|
+
return {
|
|
278
|
+
name: entryAgentName || 'Assistant',
|
|
279
|
+
emoji: entryAgentEmoji || iconForAgent(ORCHESTRATOR_AGENT_ID),
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!resolvedAgentId && currentConversation?.mode === 'plan') {
|
|
285
|
+
resolvedAgentId = ORCHESTRATOR_AGENT_ID
|
|
286
|
+
}
|
|
287
|
+
if (!resolvedAgentId && currentConversation?.mode === 'chat') {
|
|
288
|
+
resolvedAgentId = ORCHESTRATOR_AGENT_ID
|
|
289
|
+
}
|
|
290
|
+
if (!resolvedAgentId) {
|
|
291
|
+
return { name: '', emoji: '' }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const agent =
|
|
295
|
+
agentById && typeof agentById === 'object' ? agentById[resolvedAgentId] ?? null : null
|
|
296
|
+
return {
|
|
297
|
+
name:
|
|
298
|
+
(agent && typeof agent.name === 'string' && agent.name.trim()) ||
|
|
299
|
+
(resolvedAgentId === ORCHESTRATOR_AGENT_ID ? 'Orchestrator' : resolvedAgentId),
|
|
300
|
+
emoji:
|
|
301
|
+
(agent && typeof agent.emoji === 'string' && agent.emoji.trim()) ||
|
|
302
|
+
iconForAgent(resolvedAgentId),
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function buildEntriesFromThreadData(data, agentById) {
|
|
307
|
+
const timeline = []
|
|
308
|
+
const turns = Array.isArray(data?.turns) ? data.turns : []
|
|
309
|
+
const messages = Array.isArray(data?.messages) ? data.messages : []
|
|
310
|
+
const events = Array.isArray(data?.events) ? data.events : []
|
|
311
|
+
const turnsById = new Map()
|
|
312
|
+
const turnsWithAssistantTextEvents = new Set()
|
|
313
|
+
|
|
314
|
+
for (const turn of turns) {
|
|
315
|
+
if (!turn || typeof turn !== 'object') {
|
|
316
|
+
continue
|
|
317
|
+
}
|
|
318
|
+
if (typeof turn.id !== 'string' || !turn.id) {
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
turnsById.set(turn.id, turn)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const event of events) {
|
|
325
|
+
if (!event || typeof event !== 'object') {
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
if (event.event_name !== 'assistant_text' && event.event_name !== 'worker_text') {
|
|
329
|
+
continue
|
|
330
|
+
}
|
|
331
|
+
if (typeof event.turn_id !== 'string' || !event.turn_id) {
|
|
332
|
+
continue
|
|
333
|
+
}
|
|
334
|
+
turnsWithAssistantTextEvents.add(event.turn_id)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function resolveAssistantIdentity(turnId, fallbackAgentId = '') {
|
|
338
|
+
const turn = typeof turnId === 'string' && turnId ? turnsById.get(turnId) ?? null : null
|
|
339
|
+
const mode = typeof turn?.mode === 'string' ? turn.mode : ''
|
|
340
|
+
const selectedAgentId =
|
|
341
|
+
typeof turn?.selected_agent_id === 'string' ? turn.selected_agent_id : ''
|
|
342
|
+
const orchestratorAgentId =
|
|
343
|
+
typeof turn?.orchestrator_agent_id === 'string' ? turn.orchestrator_agent_id : ''
|
|
344
|
+
|
|
345
|
+
const resolvedAgentId =
|
|
346
|
+
mode === 'execute'
|
|
347
|
+
? selectedAgentId || fallbackAgentId || orchestratorAgentId || ORCHESTRATOR_AGENT_ID
|
|
348
|
+
: orchestratorAgentId || ORCHESTRATOR_AGENT_ID
|
|
349
|
+
const resolvedAgent =
|
|
350
|
+
resolvedAgentId && agentById && typeof agentById === 'object'
|
|
351
|
+
? agentById[resolvedAgentId]
|
|
352
|
+
: null
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
agentId: resolvedAgentId,
|
|
356
|
+
agentName:
|
|
357
|
+
(resolvedAgent && typeof resolvedAgent.name === 'string' && resolvedAgent.name) ||
|
|
358
|
+
(resolvedAgentId === ORCHESTRATOR_AGENT_ID ? 'Orchestrator' : '') ||
|
|
359
|
+
resolvedAgentId,
|
|
360
|
+
agentEmoji:
|
|
361
|
+
(resolvedAgent && typeof resolvedAgent.emoji === 'string' && resolvedAgent.emoji) ||
|
|
362
|
+
iconForAgent(resolvedAgentId),
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (const message of messages) {
|
|
367
|
+
if (!message || typeof message !== 'object') {
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
if (message.role !== 'user' && message.role !== 'assistant') {
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
if (typeof message.content !== 'string') {
|
|
374
|
+
throw new Error('Conversation message content must be a string.')
|
|
375
|
+
}
|
|
376
|
+
if (
|
|
377
|
+
message.role === 'assistant' &&
|
|
378
|
+
typeof message.turn_id === 'string' &&
|
|
379
|
+
message.turn_id &&
|
|
380
|
+
turnsWithAssistantTextEvents.has(message.turn_id)
|
|
381
|
+
) {
|
|
382
|
+
// Avoid duplicating assistant text when the turn already emitted streaming text events.
|
|
383
|
+
continue
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const entry = {
|
|
387
|
+
id: message.id || nextEntryId(message.role),
|
|
388
|
+
type: message.role,
|
|
389
|
+
content: message.content,
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (message.role === 'assistant') {
|
|
393
|
+
const identity = resolveAssistantIdentity(message.turn_id)
|
|
394
|
+
entry.agentId = identity.agentId
|
|
395
|
+
entry.agentName = identity.agentName
|
|
396
|
+
entry.agentEmoji = identity.agentEmoji
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
timeline.push({
|
|
400
|
+
createdAt: toTimestamp(message.created_at),
|
|
401
|
+
order: message.role === 'user' ? 1 : 2,
|
|
402
|
+
entry,
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const event of events) {
|
|
407
|
+
if (!event || typeof event !== 'object') {
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (event.event_name === 'assistant_text' || event.event_name === 'worker_text') {
|
|
412
|
+
const delta = typeof event.payload?.delta === 'string' ? event.payload.delta : ''
|
|
413
|
+
if (!delta) {
|
|
414
|
+
continue
|
|
415
|
+
}
|
|
416
|
+
const workerAgentId =
|
|
417
|
+
typeof event.payload?.worker_agent_id === 'string' ? event.payload.worker_agent_id : ''
|
|
418
|
+
const identity = resolveAssistantIdentity(event.turn_id, workerAgentId)
|
|
419
|
+
timeline.push({
|
|
420
|
+
createdAt: toTimestamp(event.created_at),
|
|
421
|
+
order: 2,
|
|
422
|
+
entry: {
|
|
423
|
+
id: event.id || nextEntryId('assistant'),
|
|
424
|
+
type: 'assistant',
|
|
425
|
+
content: delta,
|
|
426
|
+
agentId: identity.agentId,
|
|
427
|
+
agentName: identity.agentName,
|
|
428
|
+
agentEmoji: identity.agentEmoji,
|
|
429
|
+
},
|
|
430
|
+
})
|
|
431
|
+
continue
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (event.event_name === 'tool_event' || event.event_name === 'worker_tool_event') {
|
|
435
|
+
const payload = event.payload
|
|
436
|
+
const summary = summarizeToolEvent(payload?.name, payload?.payload)
|
|
437
|
+
if (!summary) {
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
timeline.push({
|
|
441
|
+
createdAt: toTimestamp(event.created_at),
|
|
442
|
+
order: 3,
|
|
443
|
+
entry: {
|
|
444
|
+
id: event.id || nextEntryId('tool'),
|
|
445
|
+
type: 'tool',
|
|
446
|
+
label: summary.label,
|
|
447
|
+
summary: summary.summary,
|
|
448
|
+
},
|
|
449
|
+
})
|
|
450
|
+
continue
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (event.event_name === 'worker_question_requested') {
|
|
454
|
+
const question = event.payload?.question || 'Question from agent'
|
|
455
|
+
const options = Array.isArray(event.payload?.options) ? event.payload.options : null
|
|
456
|
+
const details = event.payload?.details || null
|
|
457
|
+
timeline.push({
|
|
458
|
+
createdAt: toTimestamp(event.created_at),
|
|
459
|
+
order: 4,
|
|
460
|
+
entry: {
|
|
461
|
+
id: event.id || nextEntryId('question'),
|
|
462
|
+
type: 'question',
|
|
463
|
+
content: question,
|
|
464
|
+
details,
|
|
465
|
+
options,
|
|
466
|
+
},
|
|
467
|
+
})
|
|
468
|
+
continue
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const statusText = summarizeStatusEvent(event.event_name, event.payload)
|
|
472
|
+
if (statusText) {
|
|
473
|
+
timeline.push({
|
|
474
|
+
createdAt: toTimestamp(event.created_at),
|
|
475
|
+
order: 4,
|
|
476
|
+
entry: {
|
|
477
|
+
id: event.id || nextEntryId('status'),
|
|
478
|
+
type: 'status',
|
|
479
|
+
content: statusText,
|
|
480
|
+
},
|
|
481
|
+
})
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
timeline.sort((a, b) => a.createdAt - b.createdAt || a.order - b.order)
|
|
486
|
+
return groupConsecutiveToolEntries(timeline.map((item) => item.entry))
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const EVENT_DESCRIPTIONS = {
|
|
490
|
+
orchestrator_started: () => 'Orchestrator started.',
|
|
491
|
+
recipient_requested: (p) => `Manual recipient requested: ${requireEventString(p?.recipient_agent_id, 'recipient_agent_id')}`,
|
|
492
|
+
recipient_selected: (p) => `Recipient selected: ${requireEventString(p?.selected_agent_id, 'selected_agent_id')}`,
|
|
493
|
+
plan_recipient_selected: (p) => `Plan recipient selected: ${requireEventString(p?.selected_agent_id, 'selected_agent_id')}`,
|
|
494
|
+
plan_proposal_submitted: (p) => {
|
|
495
|
+
const count = Array.isArray(p?.tasks) ? p.tasks.length : 0
|
|
496
|
+
return `Plan proposal submitted with ${count} task${count !== 1 ? 's' : ''}.`
|
|
497
|
+
},
|
|
498
|
+
recipient_selected_via_cli: () => '',
|
|
499
|
+
worker_started: (p) => `Worker started: ${requireEventString(p?.worker_agent_id, 'worker_agent_id')}`,
|
|
500
|
+
recipient_required: (p) => requireEventString(p?.reason, 'reason'),
|
|
501
|
+
worker_question_requested: (p) => requireEventString(p?.question, 'question'),
|
|
502
|
+
worker_help_requested: (p) => requireEventString(p?.summary, 'summary'),
|
|
503
|
+
human_response_received: () => 'Human response received. Resuming worker.',
|
|
504
|
+
worker_completed: () => 'Worker completed.',
|
|
505
|
+
task_reopened: () => 'Task reopened. Send a follow-up message to continue.',
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function summarizeStatusEvent(name, payload) {
|
|
509
|
+
if (!name) return ''
|
|
510
|
+
const fn = EVENT_DESCRIPTIONS[name]
|
|
511
|
+
return fn ? fn(payload) : ''
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function requireEventString(value, fieldName) {
|
|
515
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
516
|
+
throw new Error(`Missing event field: ${fieldName}`)
|
|
517
|
+
}
|
|
518
|
+
return value
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function toTimestamp(value) {
|
|
522
|
+
const parsed = Date.parse(value)
|
|
523
|
+
return Number.isFinite(parsed) ? parsed : 0
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const HIDDEN_CHATS_STORAGE_KEY = 'pragma.hidden_sidebar_chats.v1'
|
|
527
|
+
|
|
528
|
+
export function loadHiddenChatsByWorkspace() {
|
|
529
|
+
if (typeof window === 'undefined') {
|
|
530
|
+
return {}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
const raw = window.localStorage.getItem(HIDDEN_CHATS_STORAGE_KEY)
|
|
535
|
+
if (!raw) {
|
|
536
|
+
return {}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const parsed = JSON.parse(raw)
|
|
540
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
541
|
+
return {}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const next = {}
|
|
545
|
+
for (const [workspace, ids] of Object.entries(parsed)) {
|
|
546
|
+
if (!Array.isArray(ids)) {
|
|
547
|
+
continue
|
|
548
|
+
}
|
|
549
|
+
const filtered = ids.filter((id) => typeof id === 'string' && id.trim())
|
|
550
|
+
if (filtered.length > 0) {
|
|
551
|
+
next[workspace] = filtered
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return next
|
|
555
|
+
} catch {
|
|
556
|
+
return {}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function saveHiddenChatsByWorkspace(value) {
|
|
561
|
+
if (typeof window === 'undefined') {
|
|
562
|
+
return
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
window.localStorage.setItem(HIDDEN_CHATS_STORAGE_KEY, JSON.stringify(value))
|
|
567
|
+
} catch {
|
|
568
|
+
// Ignore storage failures.
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export const CLI_LABELS = {
|
|
573
|
+
claude_code: 'Claude Code',
|
|
574
|
+
codex: 'Codex',
|
|
575
|
+
}
|
package/ui/src/main.jsx
ADDED