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,252 @@
1
+ import { useState, useRef, useEffect, useMemo } from 'react'
2
+ import {
3
+ fetchConversationThread,
4
+ fetchPlanProposal,
5
+ openConversationThreadStream,
6
+ } from '../api'
7
+ import {
8
+ ORCHESTRATOR_AGENT_ID,
9
+ appendAssistantDelta,
10
+ appendToolEntryStreaming,
11
+ buildEntriesFromThreadData,
12
+ hasRunningTurn,
13
+ isTaskActivelyRunning,
14
+ nextEntryId,
15
+ resolveConversationHeaderAgent,
16
+ summarizeToolEvent,
17
+ } from '../lib/conversationUtils'
18
+ import { iconForAgent } from '../lib/agentIcon'
19
+
20
+ const INITIAL_CONVERSATION = {
21
+ open: false,
22
+ mode: 'chat',
23
+ threadId: '',
24
+ taskId: '',
25
+ taskStatus: '',
26
+ taskTitle: '',
27
+ harness: '',
28
+ modelLabel: '',
29
+ reasoningEffort: 'medium',
30
+ recipientAgentId: '',
31
+ entries: [],
32
+ loading: false,
33
+ error: '',
34
+ planReady: false,
35
+ planProposal: null,
36
+ }
37
+
38
+ export { INITIAL_CONVERSATION }
39
+
40
+ export function useConversation({ agentById, tasks }) {
41
+ const [conversation, setConversation] = useState(INITIAL_CONVERSATION)
42
+
43
+ const streamAbortRef = useRef(null)
44
+ const threadIdRef = useRef(conversation.threadId)
45
+ const conversationSyncInFlightRef = useRef(false)
46
+ const conversationSyncPendingRef = useRef(false)
47
+ const conversationSyncRetryTimerRef = useRef(null)
48
+ const viewingChatIdRef = useRef('')
49
+ const prevThinkingChatIdsRef = useRef(new Set())
50
+
51
+ const conversationHeaderAgent = useMemo(() => {
52
+ return resolveConversationHeaderAgent({ conversation, tasks, agentById })
53
+ }, [conversation, tasks, agentById])
54
+
55
+ const activePlanThreadId =
56
+ conversation.open && conversation.mode === 'plan' ? conversation.threadId : ''
57
+
58
+ useEffect(() => {
59
+ threadIdRef.current = conversation.threadId
60
+ }, [conversation.threadId])
61
+
62
+ // SSE conversation thread stream
63
+ useEffect(() => {
64
+ if (
65
+ !conversation.open ||
66
+ conversation.mode !== 'chat' ||
67
+ !conversation.taskId ||
68
+ !conversation.threadId
69
+ ) {
70
+ return
71
+ }
72
+
73
+ let cancelled = false
74
+ let throttledSyncTimer = null
75
+ const scheduleConversationRetry = (delayMs = 150) => {
76
+ if (cancelled || conversationSyncRetryTimerRef.current) {
77
+ return
78
+ }
79
+ conversationSyncRetryTimerRef.current = setTimeout(() => {
80
+ conversationSyncRetryTimerRef.current = null
81
+ void syncOpenConversation()
82
+ }, delayMs)
83
+ }
84
+
85
+ const syncOpenConversation = async () => {
86
+ if (cancelled) return
87
+ if (conversationSyncInFlightRef.current) {
88
+ conversationSyncPendingRef.current = true
89
+ return
90
+ }
91
+ conversationSyncInFlightRef.current = true
92
+ try {
93
+ conversationSyncPendingRef.current = false
94
+ const currentThreadId = threadIdRef.current
95
+ if (!currentThreadId || cancelled) return
96
+ const data = await fetchConversationThread(currentThreadId)
97
+ if (!data?.thread || cancelled) return
98
+
99
+ const nextEntries = buildEntriesFromThreadData(data, agentById)
100
+ const turnsRunning = hasRunningTurn(data.turns)
101
+
102
+ let proposal = null
103
+ if (data.thread.mode === 'plan') {
104
+ try { proposal = await fetchPlanProposal(currentThreadId) } catch {}
105
+ }
106
+
107
+ setConversation((prev) => {
108
+ if (!prev.open || prev.threadId !== threadIdRef.current || cancelled) return prev
109
+ const nextLoading = prev.taskId ? isTaskActivelyRunning(prev.taskStatus) : turnsRunning
110
+ return {
111
+ ...prev,
112
+ harness: data.thread.harness,
113
+ modelLabel: data.thread.model_label,
114
+ loading: nextLoading,
115
+ entries: nextEntries,
116
+ ...(data.thread.mode === 'plan' && proposal ? { planProposal: proposal, planReady: true } : {}),
117
+ }
118
+ })
119
+ } catch {} finally {
120
+ conversationSyncInFlightRef.current = false
121
+ if (conversationSyncPendingRef.current) {
122
+ conversationSyncPendingRef.current = false
123
+ scheduleConversationRetry(150)
124
+ }
125
+ }
126
+ }
127
+
128
+ const throttledSync = () => {
129
+ if (cancelled || throttledSyncTimer) return
130
+ throttledSyncTimer = setTimeout(() => {
131
+ throttledSyncTimer = null
132
+ void syncOpenConversation()
133
+ }, 2000)
134
+ }
135
+
136
+ const resolveAgentIdentity = (workerAgentId) => {
137
+ const resolvedId = (typeof workerAgentId === 'string' && workerAgentId) || ORCHESTRATOR_AGENT_ID
138
+ const agent = agentById && typeof agentById === 'object' ? agentById[resolvedId] : null
139
+ return {
140
+ agentId: resolvedId,
141
+ agentName: (agent?.name) || (resolvedId === ORCHESTRATOR_AGENT_ID ? 'Orchestrator' : '') || resolvedId,
142
+ agentEmoji: (agent?.emoji) || iconForAgent(resolvedId),
143
+ }
144
+ }
145
+
146
+ const closeStream = openConversationThreadStream(conversation.threadId, {
147
+ onReady: () => { void syncOpenConversation() },
148
+ onThreadUpdated: () => { throttledSync() },
149
+ onEvent: ({ event, data }) => {
150
+ setConversation((prev) => {
151
+ if (!prev.open || prev.threadId !== threadIdRef.current || cancelled) return prev
152
+
153
+ if (event === 'worker_text' || event === 'assistant_text') {
154
+ const delta = typeof data?.delta === 'string' ? data.delta : ''
155
+ if (!delta) return prev
156
+ const workerAgentId = typeof data?.worker_agent_id === 'string' ? data.worker_agent_id : ''
157
+ const identity = resolveAgentIdentity(workerAgentId)
158
+ return { ...prev, entries: appendAssistantDelta(prev.entries, delta, identity) }
159
+ }
160
+
161
+ if (event === 'worker_tool_event' || event === 'tool_event') {
162
+ const summary = summarizeToolEvent(data?.name, data?.payload)
163
+ if (!summary) return prev
164
+ return {
165
+ ...prev,
166
+ entries: appendToolEntryStreaming(prev.entries, {
167
+ id: nextEntryId('tool'), type: 'tool',
168
+ label: summary.label, summary: summary.summary,
169
+ }),
170
+ }
171
+ }
172
+
173
+ if (event === 'error') {
174
+ return { ...prev, error: typeof data?.message === 'string' ? data.message : 'Task error' }
175
+ }
176
+
177
+ return prev
178
+ })
179
+ },
180
+ onError: () => {},
181
+ })
182
+
183
+ void syncOpenConversation()
184
+
185
+ return () => {
186
+ cancelled = true
187
+ if (conversationSyncRetryTimerRef.current) {
188
+ clearTimeout(conversationSyncRetryTimerRef.current)
189
+ conversationSyncRetryTimerRef.current = null
190
+ }
191
+ if (throttledSyncTimer) {
192
+ clearTimeout(throttledSyncTimer)
193
+ throttledSyncTimer = null
194
+ }
195
+ conversationSyncPendingRef.current = false
196
+ closeStream()
197
+ }
198
+ }, [conversation.open, conversation.mode, conversation.taskId, conversation.threadId, agentById])
199
+
200
+ // Sync conversation task status from tasks list
201
+ useEffect(() => {
202
+ if (!conversation.taskId) return
203
+ const currentTaskId = conversation.taskId
204
+ const match = tasks.find((task) => task.id === currentTaskId)
205
+ const nextStatus = typeof match?.status === 'string' ? match.status : ''
206
+ const nextThreadId = typeof match?.thread_id === 'string' ? match.thread_id : ''
207
+ if ((!nextStatus || nextStatus === conversation.taskStatus) && (!nextThreadId || conversation.threadId)) return
208
+
209
+ setConversation((prev) => {
210
+ if (prev.taskId !== currentTaskId) return prev
211
+ const updates = {}
212
+ if (nextStatus && prev.taskStatus !== nextStatus) {
213
+ updates.taskStatus = nextStatus
214
+ updates.loading = isTaskActivelyRunning(nextStatus)
215
+ }
216
+ if (!prev.threadId && nextThreadId) {
217
+ updates.threadId = nextThreadId
218
+ }
219
+ if (Object.keys(updates).length === 0) return prev
220
+ return { ...prev, ...updates }
221
+ })
222
+ }, [tasks, conversation.taskId, conversation.taskStatus, conversation.threadId])
223
+
224
+ // Cleanup on unmount
225
+ useEffect(() => {
226
+ return () => {
227
+ streamAbortRef.current?.abort()
228
+ if (conversationSyncRetryTimerRef.current) {
229
+ clearTimeout(conversationSyncRetryTimerRef.current)
230
+ conversationSyncRetryTimerRef.current = null
231
+ }
232
+ conversationSyncPendingRef.current = false
233
+ }
234
+ }, [])
235
+
236
+ function closeConversationDrawer() {
237
+ viewingChatIdRef.current = ''
238
+ streamAbortRef.current?.abort()
239
+ streamAbortRef.current = null
240
+ setConversation(INITIAL_CONVERSATION)
241
+ }
242
+
243
+ return {
244
+ conversation, setConversation,
245
+ conversationHeaderAgent,
246
+ activePlanThreadId,
247
+ streamAbortRef, threadIdRef,
248
+ viewingChatIdRef,
249
+ prevThinkingChatIdsRef,
250
+ closeConversationDrawer,
251
+ }
252
+ }
@@ -0,0 +1,161 @@
1
+ import { useState, useRef, useEffect, useMemo } from 'react'
2
+ import { ApiError, fetchTasks, fetchPlans, fetchChats } from '../api'
3
+ import { errorText, getPendingCount, loadHiddenChatsByWorkspace, saveHiddenChatsByWorkspace } from '../lib/conversationUtils'
4
+
5
+ export function useTasks({ activeWorkspaceName, setWorkspaceError }) {
6
+ const [tasks, setTasks] = useState([])
7
+ const [tasksLoading, setTasksLoading] = useState(false)
8
+ const [tasksError, setTasksError] = useState('')
9
+ const [taskFailureNotice, setTaskFailureNotice] = useState(null)
10
+ const [followupForTaskId, setFollowupForTaskId] = useState('')
11
+
12
+ const [sidebarPlans, setSidebarPlans] = useState([])
13
+ const [sidebarPlansLoading, setSidebarPlansLoading] = useState(false)
14
+ const [sidebarChats, setSidebarChats] = useState([])
15
+ const [sidebarChatsLoading, setSidebarChatsLoading] = useState(false)
16
+ const [hiddenChatsByWorkspace, setHiddenChatsByWorkspace] = useState(loadHiddenChatsByWorkspace)
17
+ const [unreadChatIds, setUnreadChatIds] = useState(() => new Set())
18
+
19
+ const tasksRefreshTimerRef = useRef(null)
20
+ const tasksRefreshInFlightRef = useRef(false)
21
+ const tasksRefreshQueuedRef = useRef(false)
22
+ const tasksInitialLoadDoneRef = useRef(false)
23
+ const chatsPollTimerRef = useRef(null)
24
+ const tasksRef = useRef(tasks)
25
+
26
+ const pendingCount = useMemo(() => getPendingCount(tasks), [tasks])
27
+
28
+ useEffect(() => { tasksRef.current = tasks }, [tasks])
29
+ useEffect(() => { setTaskFailureNotice(null) }, [activeWorkspaceName])
30
+ useEffect(() => { saveHiddenChatsByWorkspace(hiddenChatsByWorkspace) }, [hiddenChatsByWorkspace])
31
+
32
+ async function loadTasks() {
33
+ if (!tasksInitialLoadDoneRef.current) {
34
+ setTasksLoading(true)
35
+ }
36
+ setTasksError('')
37
+ try {
38
+ setTasks(await fetchTasks(300))
39
+ tasksInitialLoadDoneRef.current = true
40
+ } catch (error) {
41
+ if (error instanceof ApiError && error.code === 'NO_ACTIVE_WORKSPACE') {
42
+ setTasks([])
43
+ setTasksError('No active workspace.')
44
+ return
45
+ }
46
+ setTasksError(errorText(error))
47
+ } finally {
48
+ setTasksLoading(false)
49
+ }
50
+ }
51
+
52
+ async function loadPlans() {
53
+ setSidebarPlansLoading(true)
54
+ try {
55
+ setSidebarPlans(await fetchPlans(20))
56
+ } catch (error) {
57
+ if (error instanceof ApiError && error.code === 'NO_ACTIVE_WORKSPACE') {
58
+ setSidebarPlans([])
59
+ return
60
+ }
61
+ if (error instanceof ApiError && error.code === 'REQUEST_TIMEOUT') {
62
+ return
63
+ }
64
+ setWorkspaceError((prev) => prev || errorText(error))
65
+ } finally {
66
+ setSidebarPlansLoading(false)
67
+ }
68
+ }
69
+
70
+ async function loadChats({ silent = false } = {}) {
71
+ if (!silent) {
72
+ setSidebarChatsLoading(true)
73
+ }
74
+ try {
75
+ setSidebarChats(await fetchChats(20))
76
+ } catch (error) {
77
+ if (error instanceof ApiError && error.code === 'NO_ACTIVE_WORKSPACE') {
78
+ setSidebarChats([])
79
+ return
80
+ }
81
+ if (error instanceof ApiError && error.code === 'REQUEST_TIMEOUT') {
82
+ return
83
+ }
84
+ setWorkspaceError((prev) => prev || errorText(error))
85
+ } finally {
86
+ if (!silent) {
87
+ setSidebarChatsLoading(false)
88
+ }
89
+ }
90
+ }
91
+
92
+ async function flushTasksRefresh() {
93
+ if (tasksRefreshInFlightRef.current) {
94
+ tasksRefreshQueuedRef.current = true
95
+ return
96
+ }
97
+ tasksRefreshInFlightRef.current = true
98
+ try {
99
+ do {
100
+ tasksRefreshQueuedRef.current = false
101
+ await loadTasks()
102
+ } while (tasksRefreshQueuedRef.current)
103
+ } finally {
104
+ tasksRefreshInFlightRef.current = false
105
+ }
106
+ }
107
+
108
+ function scheduleTasksRefresh(delayMs = 250) {
109
+ if (tasksRefreshTimerRef.current) {
110
+ clearTimeout(tasksRefreshTimerRef.current)
111
+ }
112
+ tasksRefreshTimerRef.current = setTimeout(() => {
113
+ tasksRefreshTimerRef.current = null
114
+ void flushTasksRefresh()
115
+ }, delayMs)
116
+ }
117
+
118
+ function clearTasksData() {
119
+ setTasks([])
120
+ tasksInitialLoadDoneRef.current = false
121
+ setTasksError('')
122
+ setSidebarPlans([])
123
+ setSidebarPlansLoading(false)
124
+ setSidebarChats([])
125
+ setSidebarChatsLoading(false)
126
+ if (chatsPollTimerRef.current) {
127
+ clearInterval(chatsPollTimerRef.current)
128
+ chatsPollTimerRef.current = null
129
+ }
130
+ }
131
+
132
+ // Cleanup on unmount
133
+ useEffect(() => {
134
+ return () => {
135
+ if (tasksRefreshTimerRef.current) {
136
+ clearTimeout(tasksRefreshTimerRef.current)
137
+ tasksRefreshTimerRef.current = null
138
+ }
139
+ if (chatsPollTimerRef.current) {
140
+ clearInterval(chatsPollTimerRef.current)
141
+ chatsPollTimerRef.current = null
142
+ }
143
+ tasksRefreshQueuedRef.current = false
144
+ }
145
+ }, [])
146
+
147
+ return {
148
+ tasks, setTasks,
149
+ tasksLoading, tasksError, setTasksError,
150
+ taskFailureNotice, setTaskFailureNotice,
151
+ followupForTaskId, setFollowupForTaskId,
152
+ pendingCount,
153
+ sidebarPlans, setSidebarPlans, sidebarPlansLoading,
154
+ sidebarChats, sidebarChatsLoading,
155
+ hiddenChatsByWorkspace, setHiddenChatsByWorkspace,
156
+ unreadChatIds, setUnreadChatIds,
157
+ tasksRef, chatsPollTimerRef,
158
+ loadTasks, loadPlans, loadChats,
159
+ scheduleTasksRefresh, clearTasksData,
160
+ }
161
+ }
@@ -0,0 +1,259 @@
1
+ import { useState, useRef, useEffect } from 'react'
2
+ import {
3
+ ApiError,
4
+ fetchWorkspaces,
5
+ fetchContextFiles,
6
+ fetchCodeFolders,
7
+ fetchRuntimeServices,
8
+ openRuntimeServiceStream,
9
+ } from '../api'
10
+ import { errorText } from '../lib/conversationUtils'
11
+
12
+ export function useWorkspace() {
13
+ const [workspaces, setWorkspaces] = useState([])
14
+ const [activeWorkspaceName, setActiveWorkspaceName] = useState('')
15
+ const [workspacesLoading, setWorkspacesLoading] = useState(false)
16
+ const [workspaceError, setWorkspaceError] = useState('')
17
+
18
+ const [isOnboardingOpen, setIsOnboardingOpen] = useState(false)
19
+ const [onboardingError, setOnboardingError] = useState('')
20
+ const [onboardingLoading, setOnboardingLoading] = useState(false)
21
+ const [deleteWorkspaceLoading, setDeleteWorkspaceLoading] = useState(false)
22
+ const [deleteWorkspaceError, setDeleteWorkspaceError] = useState('')
23
+ const [openOrchestratorConfigRequest, setOpenOrchestratorConfigRequest] = useState(0)
24
+
25
+ const [contextData, setContextData] = useState({ folders: [], files: [] })
26
+ const [contextLoading, setContextLoading] = useState(false)
27
+ const [contextError, setContextError] = useState('')
28
+ const [codeFolders, setCodeFolders] = useState([])
29
+ const [codeLoading, setCodeLoading] = useState(false)
30
+ const [codeError, setCodeError] = useState('')
31
+
32
+ const [runtimeServices, setRuntimeServices] = useState([])
33
+ const [selectedServiceId, setSelectedServiceId] = useState('')
34
+ const [runtimeServiceLogsById, setRuntimeServiceLogsById] = useState(() => ({}))
35
+ const [runtimeServiceStreamError, setRuntimeServiceStreamError] = useState('')
36
+
37
+ const runtimeServicesPollTimerRef = useRef(null)
38
+ const runtimeServiceStreamCloseRef = useRef(null)
39
+
40
+ const hasAnyWorkspace = workspaces.length > 0
41
+
42
+ async function refreshWorkspaces() {
43
+ const next = await fetchWorkspaces()
44
+ setWorkspaces(next)
45
+ const active = next.find((ws) => ws.active)?.name || ''
46
+ setActiveWorkspaceName(active)
47
+ return { next, active }
48
+ }
49
+
50
+ async function loadContext() {
51
+ setContextLoading(true)
52
+ setContextError('')
53
+ try {
54
+ setContextData(await fetchContextFiles())
55
+ } catch (error) {
56
+ if (error instanceof ApiError && error.code === 'NO_ACTIVE_WORKSPACE') {
57
+ setContextData({ folders: [], files: [] })
58
+ setContextError('No active workspace.')
59
+ return
60
+ }
61
+ setContextError(errorText(error))
62
+ } finally {
63
+ setContextLoading(false)
64
+ }
65
+ }
66
+
67
+ async function loadCode() {
68
+ setCodeLoading(true)
69
+ setCodeError('')
70
+ try {
71
+ setCodeFolders(await fetchCodeFolders())
72
+ } catch (error) {
73
+ if (error instanceof ApiError && error.code === 'NO_ACTIVE_WORKSPACE') {
74
+ setCodeFolders([])
75
+ setCodeError('No active workspace.')
76
+ return
77
+ }
78
+ setCodeError(errorText(error))
79
+ } finally {
80
+ setCodeLoading(false)
81
+ }
82
+ }
83
+
84
+ async function loadRuntimeServices() {
85
+ try {
86
+ const next = await fetchRuntimeServices()
87
+ setRuntimeServices(next)
88
+ if (selectedServiceId && !next.some((service) => service.id === selectedServiceId)) {
89
+ setSelectedServiceId('')
90
+ setRuntimeServiceStreamError('')
91
+ }
92
+ } catch (error) {
93
+ if (error instanceof ApiError && error.code === 'NO_ACTIVE_WORKSPACE') {
94
+ setRuntimeServices([])
95
+ setSelectedServiceId('')
96
+ return
97
+ }
98
+ if (error instanceof ApiError && error.code === 'REQUEST_TIMEOUT') {
99
+ return
100
+ }
101
+ }
102
+ }
103
+
104
+ function upsertRuntimeService(nextService) {
105
+ if (!nextService || typeof nextService !== 'object' || !nextService.id) {
106
+ return
107
+ }
108
+ setRuntimeServices((prev) => {
109
+ const index = prev.findIndex((service) => service.id === nextService.id)
110
+ if (index === -1) {
111
+ return [nextService, ...prev]
112
+ }
113
+ const next = [...prev]
114
+ next[index] = { ...next[index], ...nextService }
115
+ return next
116
+ })
117
+ }
118
+
119
+ function clearWorkspaceData() {
120
+ setContextData({ folders: [], files: [] })
121
+ setCodeFolders([])
122
+ setCodeLoading(false)
123
+ setContextError('')
124
+ setCodeError('')
125
+ setRuntimeServices([])
126
+ setSelectedServiceId('')
127
+ setRuntimeServiceLogsById({})
128
+ setRuntimeServiceStreamError('')
129
+ runtimeServiceStreamCloseRef.current?.()
130
+ runtimeServiceStreamCloseRef.current = null
131
+ if (runtimeServicesPollTimerRef.current) {
132
+ clearInterval(runtimeServicesPollTimerRef.current)
133
+ runtimeServicesPollTimerRef.current = null
134
+ }
135
+ }
136
+
137
+ // Runtime services polling
138
+ useEffect(() => {
139
+ if (!activeWorkspaceName) {
140
+ setRuntimeServices([])
141
+ setSelectedServiceId('')
142
+ setRuntimeServiceLogsById({})
143
+ setRuntimeServiceStreamError('')
144
+ return
145
+ }
146
+
147
+ void loadRuntimeServices()
148
+
149
+ if (runtimeServicesPollTimerRef.current) {
150
+ clearInterval(runtimeServicesPollTimerRef.current)
151
+ }
152
+ runtimeServicesPollTimerRef.current = setInterval(() => {
153
+ void loadRuntimeServices()
154
+ }, 3000)
155
+
156
+ return () => {
157
+ if (runtimeServicesPollTimerRef.current) {
158
+ clearInterval(runtimeServicesPollTimerRef.current)
159
+ runtimeServicesPollTimerRef.current = null
160
+ }
161
+ }
162
+ }, [activeWorkspaceName])
163
+
164
+ // Runtime service stream
165
+ useEffect(() => {
166
+ runtimeServiceStreamCloseRef.current?.()
167
+ runtimeServiceStreamCloseRef.current = null
168
+ setRuntimeServiceStreamError('')
169
+
170
+ if (!selectedServiceId) {
171
+ return
172
+ }
173
+
174
+ const close = openRuntimeServiceStream(selectedServiceId, {
175
+ onReady: (payload) => {
176
+ if (!payload || typeof payload !== 'object') {
177
+ return
178
+ }
179
+ if (payload.service && typeof payload.service === 'object') {
180
+ upsertRuntimeService(payload.service)
181
+ }
182
+ if (Array.isArray(payload.logs)) {
183
+ setRuntimeServiceLogsById((prev) => ({
184
+ ...prev,
185
+ [selectedServiceId]: payload.logs,
186
+ }))
187
+ }
188
+ },
189
+ onLog: (payload) => {
190
+ const entry = payload?.entry
191
+ if (!entry || typeof entry !== 'object') {
192
+ return
193
+ }
194
+ setRuntimeServiceLogsById((prev) => {
195
+ const existing = Array.isArray(prev[selectedServiceId]) ? prev[selectedServiceId] : []
196
+ const nextLogs = [...existing, entry]
197
+ if (nextLogs.length > 2000) {
198
+ nextLogs.splice(0, nextLogs.length - 2000)
199
+ }
200
+ return {
201
+ ...prev,
202
+ [selectedServiceId]: nextLogs,
203
+ }
204
+ })
205
+ },
206
+ onStatus: (payload) => {
207
+ const nextService = payload?.service
208
+ if (!nextService || typeof nextService !== 'object') {
209
+ return
210
+ }
211
+ upsertRuntimeService(nextService)
212
+ },
213
+ onError: () => {
214
+ setRuntimeServiceStreamError('Service log stream disconnected.')
215
+ },
216
+ })
217
+
218
+ runtimeServiceStreamCloseRef.current = close
219
+ return () => {
220
+ close()
221
+ if (runtimeServiceStreamCloseRef.current === close) {
222
+ runtimeServiceStreamCloseRef.current = null
223
+ }
224
+ }
225
+ }, [selectedServiceId])
226
+
227
+ // Cleanup on unmount
228
+ useEffect(() => {
229
+ return () => {
230
+ if (runtimeServicesPollTimerRef.current) {
231
+ clearInterval(runtimeServicesPollTimerRef.current)
232
+ runtimeServicesPollTimerRef.current = null
233
+ }
234
+ runtimeServiceStreamCloseRef.current?.()
235
+ runtimeServiceStreamCloseRef.current = null
236
+ }
237
+ }, [])
238
+
239
+ return {
240
+ workspaces, setWorkspaces,
241
+ activeWorkspaceName, setActiveWorkspaceName,
242
+ workspacesLoading, setWorkspacesLoading,
243
+ workspaceError, setWorkspaceError,
244
+ hasAnyWorkspace,
245
+ isOnboardingOpen, setIsOnboardingOpen,
246
+ onboardingError, setOnboardingError,
247
+ onboardingLoading, setOnboardingLoading,
248
+ deleteWorkspaceLoading, setDeleteWorkspaceLoading,
249
+ deleteWorkspaceError, setDeleteWorkspaceError,
250
+ openOrchestratorConfigRequest, setOpenOrchestratorConfigRequest,
251
+ contextData, setContextData, contextLoading, contextError, setContextError,
252
+ codeFolders, setCodeFolders, codeLoading, setCodeLoading, codeError, setCodeError,
253
+ runtimeServices, selectedServiceId, setSelectedServiceId,
254
+ runtimeServiceLogsById, runtimeServiceStreamError, setRuntimeServiceStreamError,
255
+ refreshWorkspaces, loadContext, loadCode, loadRuntimeServices,
256
+ upsertRuntimeService, clearWorkspaceData,
257
+ runtimeServicesPollTimerRef,
258
+ }
259
+ }
@@ -0,0 +1,10 @@
1
+ const AGENT_ICONS = ['๐Ÿ”Ž', 'โœ๏ธ', '๐Ÿ“Š', '๐Ÿง ', '๐Ÿ› ', '๐Ÿงช', '๐Ÿ›ฐ๏ธ', '๐Ÿงฉ']
2
+
3
+ export function iconForAgent(id) {
4
+ const text = String(id ?? '')
5
+ let hash = 0
6
+ for (let i = 0; i < text.length; i += 1) {
7
+ hash = (hash * 31 + text.charCodeAt(i)) >>> 0
8
+ }
9
+ return AGENT_ICONS[hash % AGENT_ICONS.length]
10
+ }