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,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
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import App from './App'
4
+ import './styles.css'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )