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,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
|
+
}
|