better-codex 0.1.4 → 0.2.1
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/apps/backend/README.md +43 -2
- package/apps/backend/src/core/app-server.ts +68 -2
- package/apps/backend/src/core/jsonrpc.ts +13 -0
- package/apps/backend/src/server.ts +156 -1
- package/apps/backend/src/services/codex-config.ts +561 -0
- package/apps/backend/src/thread-activity/service.ts +47 -0
- package/apps/web/README.md +18 -2
- package/apps/web/src/components/layout/codex-settings.tsx +1208 -0
- package/apps/web/src/components/layout/session-view.tsx +203 -8
- package/apps/web/src/components/layout/settings-dialog.tsx +9 -1
- package/apps/web/src/components/layout/virtualized-message-list.tsx +594 -90
- package/apps/web/src/components/session-view/rate-limit-banner.tsx +178 -0
- package/apps/web/src/components/session-view/session-dialogs.tsx +93 -2
- package/apps/web/src/components/session-view/session-header.tsx +21 -2
- package/apps/web/src/components/session-view/thread-account-switcher.tsx +191 -0
- package/apps/web/src/components/ui/icons.tsx +12 -0
- package/apps/web/src/hooks/use-hub-connection.ts +59 -19
- package/apps/web/src/hooks/use-thread-history.ts +94 -5
- package/apps/web/src/services/hub-client.ts +98 -1
- package/apps/web/src/store/index.ts +36 -1
- package/apps/web/src/types/index.ts +25 -0
- package/apps/web/src/utils/item-format.ts +55 -9
- package/apps/web/src/utils/slash-commands.ts +2 -0
- package/package.json +1 -1
|
@@ -78,10 +78,16 @@ export const useHubConnection = () => {
|
|
|
78
78
|
setAccountLoginId,
|
|
79
79
|
upsertReviewSession,
|
|
80
80
|
updateReviewSession,
|
|
81
|
+
resolveThreadId,
|
|
81
82
|
} = useAppStore()
|
|
82
83
|
|
|
83
84
|
const nowTimestamp = () => new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
84
85
|
|
|
86
|
+
const resolve = (backendThreadId: string | undefined): string | undefined => {
|
|
87
|
+
if (!backendThreadId) return undefined
|
|
88
|
+
return resolveThreadId(backendThreadId)
|
|
89
|
+
}
|
|
90
|
+
|
|
85
91
|
const addSystemMessage = (threadId: string, title: string, content: string) => {
|
|
86
92
|
addMessage(threadId, {
|
|
87
93
|
id: `sys-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
@@ -238,7 +244,8 @@ export const useHubConnection = () => {
|
|
|
238
244
|
}
|
|
239
245
|
|
|
240
246
|
if (method === 'turn/started' && params && typeof params === 'object') {
|
|
241
|
-
const { threadId, turn } = params as { threadId?: string; turn?: { id?: string } }
|
|
247
|
+
const { threadId: rawThreadId, turn } = params as { threadId?: string; turn?: { id?: string } }
|
|
248
|
+
const threadId = resolve(rawThreadId)
|
|
242
249
|
if (threadId) {
|
|
243
250
|
updateThread(threadId, { status: 'active' })
|
|
244
251
|
setThreadTurnStartedAt(threadId, Date.now())
|
|
@@ -249,7 +256,8 @@ export const useHubConnection = () => {
|
|
|
249
256
|
}
|
|
250
257
|
|
|
251
258
|
if (method === 'turn/completed' && params && typeof params === 'object') {
|
|
252
|
-
const { threadId, turn } = params as { threadId?: string; turn?: { id?: string; status?: string } }
|
|
259
|
+
const { threadId: rawThreadId, turn } = params as { threadId?: string; turn?: { id?: string; status?: string } }
|
|
260
|
+
const threadId = resolve(rawThreadId)
|
|
253
261
|
// console.log('[HubConnection] turn/completed event for thread:', threadId)
|
|
254
262
|
if (threadId) {
|
|
255
263
|
const startedAt = useAppStore.getState().threadTurnStartedAt[threadId]
|
|
@@ -271,27 +279,30 @@ export const useHubConnection = () => {
|
|
|
271
279
|
}
|
|
272
280
|
|
|
273
281
|
if (method === 'turn/diff/updated' && params && typeof params === 'object') {
|
|
274
|
-
const { threadId, turnId, diff } = params as { threadId?: string; turnId?: string; diff?: string }
|
|
282
|
+
const { threadId: rawThreadId, turnId, diff } = params as { threadId?: string; turnId?: string; diff?: string }
|
|
283
|
+
const threadId = resolve(rawThreadId)
|
|
275
284
|
if (threadId && diff) {
|
|
276
285
|
const content = diff.length > 4000 ? `${diff.slice(0, 4000)}\n…` : diff
|
|
277
286
|
upsertMessage(threadId, {
|
|
278
287
|
id: `diff-${turnId ?? threadId}`,
|
|
279
288
|
role: 'assistant',
|
|
280
289
|
kind: 'file',
|
|
281
|
-
title: 'Diff
|
|
290
|
+
title: 'Diff',
|
|
282
291
|
content,
|
|
292
|
+
meta: { diff },
|
|
283
293
|
timestamp: nowTimestamp(),
|
|
284
294
|
})
|
|
285
295
|
}
|
|
286
296
|
}
|
|
287
297
|
|
|
288
298
|
if (method === 'turn/plan/updated' && params && typeof params === 'object') {
|
|
289
|
-
const { threadId, turnId, plan, explanation } = params as {
|
|
299
|
+
const { threadId: rawThreadId, turnId, plan, explanation } = params as {
|
|
290
300
|
threadId?: string
|
|
291
301
|
turnId?: string
|
|
292
302
|
plan?: Array<{ step?: string; status?: string }>
|
|
293
303
|
explanation?: string
|
|
294
304
|
}
|
|
305
|
+
const threadId = resolve(rawThreadId)
|
|
295
306
|
if (threadId && Array.isArray(plan)) {
|
|
296
307
|
const steps = plan
|
|
297
308
|
.map((entry) => `${entry.status ?? 'pending'} · ${entry.step ?? ''}`.trim())
|
|
@@ -310,7 +321,8 @@ export const useHubConnection = () => {
|
|
|
310
321
|
}
|
|
311
322
|
|
|
312
323
|
if (method === 'thread/tokenUsage/updated' && params && typeof params === 'object') {
|
|
313
|
-
const { threadId } = params as { threadId?: string }
|
|
324
|
+
const { threadId: rawThreadId } = params as { threadId?: string }
|
|
325
|
+
const threadId = resolve(rawThreadId)
|
|
314
326
|
const usage = (params as { usage?: unknown }).usage ?? (params as { tokenUsage?: unknown }).tokenUsage
|
|
315
327
|
if (threadId && usage) {
|
|
316
328
|
setThreadTokenUsage(threadId, usage)
|
|
@@ -318,73 +330,80 @@ export const useHubConnection = () => {
|
|
|
318
330
|
}
|
|
319
331
|
|
|
320
332
|
if (method === 'item/agentMessage/delta' && params && typeof params === 'object') {
|
|
321
|
-
const { threadId, itemId, delta } = params as {
|
|
333
|
+
const { threadId: rawThreadId, itemId, delta } = params as {
|
|
322
334
|
threadId?: string
|
|
323
335
|
itemId?: string
|
|
324
336
|
delta?: string
|
|
325
337
|
}
|
|
338
|
+
const threadId = resolve(rawThreadId)
|
|
326
339
|
if (threadId && itemId && delta) {
|
|
327
340
|
appendMessageDelta(threadId, itemId, delta)
|
|
328
341
|
}
|
|
329
342
|
}
|
|
330
343
|
|
|
331
344
|
if (method === 'item/reasoning/summaryTextDelta' && params && typeof params === 'object') {
|
|
332
|
-
const { threadId, itemId, delta } = params as {
|
|
345
|
+
const { threadId: rawThreadId, itemId, delta } = params as {
|
|
333
346
|
threadId?: string
|
|
334
347
|
itemId?: string
|
|
335
348
|
delta?: string
|
|
336
349
|
}
|
|
350
|
+
const threadId = resolve(rawThreadId)
|
|
337
351
|
if (threadId && itemId && delta) {
|
|
338
352
|
appendMessageDelta(threadId, itemId, delta)
|
|
339
353
|
}
|
|
340
354
|
}
|
|
341
355
|
|
|
342
356
|
if (method === 'item/reasoning/summaryPartAdded' && params && typeof params === 'object') {
|
|
343
|
-
const { threadId, itemId } = params as { threadId?: string; itemId?: string }
|
|
357
|
+
const { threadId: rawThreadId, itemId } = params as { threadId?: string; itemId?: string }
|
|
358
|
+
const threadId = resolve(rawThreadId)
|
|
344
359
|
if (threadId && itemId) {
|
|
345
360
|
appendMessageDelta(threadId, itemId, '\n\n')
|
|
346
361
|
}
|
|
347
362
|
}
|
|
348
363
|
|
|
349
364
|
if (method === 'item/reasoning/textDelta' && params && typeof params === 'object') {
|
|
350
|
-
const { threadId, itemId, delta } = params as {
|
|
365
|
+
const { threadId: rawThreadId, itemId, delta } = params as {
|
|
351
366
|
threadId?: string
|
|
352
367
|
itemId?: string
|
|
353
368
|
delta?: string
|
|
354
369
|
}
|
|
370
|
+
const threadId = resolve(rawThreadId)
|
|
355
371
|
if (threadId && itemId && delta) {
|
|
356
372
|
appendMessageDelta(threadId, itemId, delta)
|
|
357
373
|
}
|
|
358
374
|
}
|
|
359
375
|
|
|
360
376
|
if (method === 'item/commandExecution/outputDelta' && params && typeof params === 'object') {
|
|
361
|
-
const { threadId, itemId, delta } = params as {
|
|
377
|
+
const { threadId: rawThreadId, itemId, delta } = params as {
|
|
362
378
|
threadId?: string
|
|
363
379
|
itemId?: string
|
|
364
380
|
delta?: string
|
|
365
381
|
}
|
|
382
|
+
const threadId = resolve(rawThreadId)
|
|
366
383
|
if (threadId && itemId && delta) {
|
|
367
384
|
appendMessageDelta(threadId, itemId, delta)
|
|
368
385
|
}
|
|
369
386
|
}
|
|
370
387
|
|
|
371
388
|
if (method === 'item/fileChange/outputDelta' && params && typeof params === 'object') {
|
|
372
|
-
const { threadId, itemId, delta } = params as {
|
|
389
|
+
const { threadId: rawThreadId, itemId, delta } = params as {
|
|
373
390
|
threadId?: string
|
|
374
391
|
itemId?: string
|
|
375
392
|
delta?: string
|
|
376
393
|
}
|
|
394
|
+
const threadId = resolve(rawThreadId)
|
|
377
395
|
if (threadId && itemId && delta) {
|
|
378
396
|
appendMessageDelta(threadId, itemId, delta)
|
|
379
397
|
}
|
|
380
398
|
}
|
|
381
399
|
|
|
382
400
|
if (method === 'item/started' && params && typeof params === 'object') {
|
|
383
|
-
const { item, threadId, turnId } = params as {
|
|
401
|
+
const { item, threadId: rawThreadId, turnId } = params as {
|
|
384
402
|
item?: ItemPayload & { review?: string }
|
|
385
403
|
threadId?: string
|
|
386
404
|
turnId?: string
|
|
387
405
|
}
|
|
406
|
+
const threadId = resolve(rawThreadId)
|
|
388
407
|
if (threadId && item?.type === 'agentMessage' && item.id) {
|
|
389
408
|
ensureAssistantMessage(threadId, item.id)
|
|
390
409
|
return
|
|
@@ -410,11 +429,12 @@ export const useHubConnection = () => {
|
|
|
410
429
|
}
|
|
411
430
|
|
|
412
431
|
if (method === 'item/completed' && params && typeof params === 'object') {
|
|
413
|
-
const { item, threadId, turnId } = params as {
|
|
432
|
+
const { item, threadId: rawThreadId, turnId } = params as {
|
|
414
433
|
item?: ItemPayload & { review?: string }
|
|
415
434
|
threadId?: string
|
|
416
435
|
turnId?: string
|
|
417
436
|
}
|
|
437
|
+
const threadId = resolve(rawThreadId)
|
|
418
438
|
if (threadId && item?.type === 'exitedReviewMode') {
|
|
419
439
|
const sessionId = turnId ?? item.id
|
|
420
440
|
if (sessionId) {
|
|
@@ -435,7 +455,8 @@ export const useHubConnection = () => {
|
|
|
435
455
|
}
|
|
436
456
|
|
|
437
457
|
if (method === 'error' && params && typeof params === 'object') {
|
|
438
|
-
const { threadId, error } = params as { threadId?: string; error?: { message?: string } }
|
|
458
|
+
const { threadId: rawThreadId, error } = params as { threadId?: string; error?: { message?: string } }
|
|
459
|
+
const threadId = resolve(rawThreadId)
|
|
439
460
|
if (threadId) {
|
|
440
461
|
const message = error?.message ?? 'Unknown error.'
|
|
441
462
|
addSystemMessage(threadId, 'Error', message)
|
|
@@ -466,11 +487,12 @@ export const useHubConnection = () => {
|
|
|
466
487
|
parsedCmd?: string
|
|
467
488
|
command?: string[]
|
|
468
489
|
}
|
|
490
|
+
const resolvedThreadId = resolve(parsed.threadId) ?? ''
|
|
469
491
|
addApproval({
|
|
470
492
|
id: parsed.itemId ?? String(id),
|
|
471
493
|
requestId: id,
|
|
472
494
|
profileId,
|
|
473
|
-
threadId:
|
|
495
|
+
threadId: resolvedThreadId,
|
|
474
496
|
type: 'command',
|
|
475
497
|
payload: parsed.parsedCmd ?? parsed.command?.join(' ') ?? 'Command approval required',
|
|
476
498
|
status: 'pending',
|
|
@@ -483,11 +505,12 @@ export const useHubConnection = () => {
|
|
|
483
505
|
threadId?: string
|
|
484
506
|
reason?: string
|
|
485
507
|
}
|
|
508
|
+
const resolvedThreadId = resolve(parsed.threadId) ?? ''
|
|
486
509
|
addApproval({
|
|
487
510
|
id: parsed.itemId ?? String(id),
|
|
488
511
|
requestId: id,
|
|
489
512
|
profileId,
|
|
490
|
-
threadId:
|
|
513
|
+
threadId: resolvedThreadId,
|
|
491
514
|
type: 'file',
|
|
492
515
|
payload: parsed.reason ?? 'File changes requested',
|
|
493
516
|
status: 'pending',
|
|
@@ -537,8 +560,25 @@ export const useHubConnection = () => {
|
|
|
537
560
|
})
|
|
538
561
|
if (threads) {
|
|
539
562
|
setThreadsForAccount(profile.id, toThreads(profile.id, threads))
|
|
540
|
-
|
|
541
|
-
|
|
563
|
+
}
|
|
564
|
+
if (threads?.data?.length) {
|
|
565
|
+
try {
|
|
566
|
+
const activeThreads = await hubClient.listActiveThreads({ profileId: profile.id })
|
|
567
|
+
const knownThreadIds = new Set(threads.data.map((thread) => thread.id))
|
|
568
|
+
for (const entry of activeThreads) {
|
|
569
|
+
if (!knownThreadIds.has(entry.threadId)) {
|
|
570
|
+
continue
|
|
571
|
+
}
|
|
572
|
+
updateThread(entry.threadId, { status: 'active' })
|
|
573
|
+
if (entry.turnId) {
|
|
574
|
+
setThreadTurnId(entry.threadId, entry.turnId)
|
|
575
|
+
}
|
|
576
|
+
if (Number.isFinite(entry.startedAt)) {
|
|
577
|
+
setThreadTurnStartedAt(entry.threadId, entry.startedAt)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
} catch (error) {
|
|
581
|
+
console.error(error)
|
|
542
582
|
}
|
|
543
583
|
}
|
|
544
584
|
|
|
@@ -24,6 +24,7 @@ type ThreadData = {
|
|
|
24
24
|
type TurnData = {
|
|
25
25
|
id: string
|
|
26
26
|
items?: ThreadItem[]
|
|
27
|
+
status?: string
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
type ThreadItem = {
|
|
@@ -109,6 +110,72 @@ const buildMessagesFromTurns = (turns: TurnData[] = []): Message[] => {
|
|
|
109
110
|
return messages
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
const mergeMessages = (base: Message[], incoming: Message[]): Message[] => {
|
|
114
|
+
if (incoming.length === 0) {
|
|
115
|
+
return base
|
|
116
|
+
}
|
|
117
|
+
if (base.length === 0) {
|
|
118
|
+
return incoming
|
|
119
|
+
}
|
|
120
|
+
const merged = [...base]
|
|
121
|
+
const indexById = new Map(base.map((msg, index) => [msg.id, index]))
|
|
122
|
+
const userContentIndex = new Map<string, number[]>()
|
|
123
|
+
|
|
124
|
+
const contentKey = (message: Message) => message.content.trim()
|
|
125
|
+
const isUserChat = (message: Message) => message.role === 'user' && message.kind === 'chat'
|
|
126
|
+
|
|
127
|
+
base.forEach((message, index) => {
|
|
128
|
+
if (!isUserChat(message) || message.timestamp) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
const key = contentKey(message)
|
|
132
|
+
if (!key) {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
const existing = userContentIndex.get(key)
|
|
136
|
+
if (existing) {
|
|
137
|
+
existing.push(index)
|
|
138
|
+
} else {
|
|
139
|
+
userContentIndex.set(key, [index])
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
for (const message of incoming) {
|
|
144
|
+
const existingIndex = indexById.get(message.id)
|
|
145
|
+
if (existingIndex === undefined) {
|
|
146
|
+
if (isUserChat(message)) {
|
|
147
|
+
const key = contentKey(message)
|
|
148
|
+
const candidates = key ? userContentIndex.get(key) : undefined
|
|
149
|
+
const targetIndex = candidates?.shift()
|
|
150
|
+
if (targetIndex !== undefined) {
|
|
151
|
+
const target = merged[targetIndex]
|
|
152
|
+
merged[targetIndex] = {
|
|
153
|
+
...target,
|
|
154
|
+
...message,
|
|
155
|
+
id: target.id,
|
|
156
|
+
timestamp: message.timestamp || target.timestamp,
|
|
157
|
+
}
|
|
158
|
+
if (candidates && candidates.length === 0) {
|
|
159
|
+
userContentIndex.delete(key)
|
|
160
|
+
}
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
merged.push(message)
|
|
165
|
+
} else {
|
|
166
|
+
merged[existingIndex] = { ...merged[existingIndex], ...message }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return merged
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const isTurnInProgress = (status?: string) => {
|
|
173
|
+
if (!status) {
|
|
174
|
+
return false
|
|
175
|
+
}
|
|
176
|
+
return status === 'inProgress' || status === 'in_progress' || status === 'inprogress'
|
|
177
|
+
}
|
|
178
|
+
|
|
112
179
|
export const useThreadHistory = () => {
|
|
113
180
|
const {
|
|
114
181
|
threads,
|
|
@@ -121,9 +188,11 @@ export const useThreadHistory = () => {
|
|
|
121
188
|
setThreadEffort,
|
|
122
189
|
setThreadApproval,
|
|
123
190
|
setThreadCwd,
|
|
191
|
+
setThreadTurnId,
|
|
124
192
|
} = useAppStore()
|
|
125
193
|
|
|
126
194
|
const inFlight = useRef<Set<string>>(new Set())
|
|
195
|
+
const loaded = useRef<Set<string>>(new Set())
|
|
127
196
|
|
|
128
197
|
useEffect(() => {
|
|
129
198
|
if (!selectedThreadId || connectionStatus !== 'connected') {
|
|
@@ -135,8 +204,7 @@ export const useThreadHistory = () => {
|
|
|
135
204
|
return
|
|
136
205
|
}
|
|
137
206
|
|
|
138
|
-
|
|
139
|
-
if (existing !== undefined) {
|
|
207
|
+
if (loaded.current.has(selectedThreadId)) {
|
|
140
208
|
return
|
|
141
209
|
}
|
|
142
210
|
|
|
@@ -162,14 +230,32 @@ export const useThreadHistory = () => {
|
|
|
162
230
|
return
|
|
163
231
|
}
|
|
164
232
|
|
|
165
|
-
const
|
|
166
|
-
|
|
233
|
+
const turns = resumeThread.turns ?? []
|
|
234
|
+
const loadedMessages = buildMessagesFromTurns(turns)
|
|
235
|
+
const currentMessages = useAppStore.getState().messages[selectedThreadId] ?? []
|
|
236
|
+
const mergedMessages = mergeMessages(loadedMessages, currentMessages)
|
|
237
|
+
setMessagesForThread(selectedThreadId, mergedMessages)
|
|
238
|
+
|
|
239
|
+
let activeTurn: TurnData | null = null
|
|
240
|
+
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
241
|
+
if (isTurnInProgress(turns[index]?.status)) {
|
|
242
|
+
activeTurn = turns[index]
|
|
243
|
+
break
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
setThreadTurnId(selectedThreadId, activeTurn?.id ?? null)
|
|
247
|
+
const nextStatus = thread.status === 'archived'
|
|
248
|
+
? 'archived'
|
|
249
|
+
: activeTurn
|
|
250
|
+
? 'active'
|
|
251
|
+
: 'idle'
|
|
167
252
|
|
|
168
253
|
updateThread(selectedThreadId, {
|
|
169
254
|
title: resumeThread.preview?.trim() || thread.title,
|
|
170
255
|
preview: resumeThread.preview?.trim() || thread.preview,
|
|
171
256
|
model: resumeThread.modelProvider ?? thread.model,
|
|
172
|
-
messageCount:
|
|
257
|
+
messageCount: mergedMessages.length,
|
|
258
|
+
status: nextStatus,
|
|
173
259
|
})
|
|
174
260
|
if (result.model) {
|
|
175
261
|
setThreadModel(selectedThreadId, result.model)
|
|
@@ -184,6 +270,7 @@ export const useThreadHistory = () => {
|
|
|
184
270
|
if (result.cwd) {
|
|
185
271
|
setThreadCwd(selectedThreadId, result.cwd)
|
|
186
272
|
}
|
|
273
|
+
loaded.current.add(selectedThreadId)
|
|
187
274
|
} catch (error) {
|
|
188
275
|
console.error(error)
|
|
189
276
|
} finally {
|
|
@@ -206,5 +293,7 @@ export const useThreadHistory = () => {
|
|
|
206
293
|
setThreadApproval,
|
|
207
294
|
threads,
|
|
208
295
|
updateThread,
|
|
296
|
+
setThreadTurnId,
|
|
297
|
+
setThreadCwd,
|
|
209
298
|
])
|
|
210
299
|
}
|
|
@@ -12,6 +12,32 @@ export type PromptSummary = {
|
|
|
12
12
|
description?: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export type McpServerConfig = {
|
|
16
|
+
name: string
|
|
17
|
+
command?: string
|
|
18
|
+
args?: string[]
|
|
19
|
+
env?: Record<string, string>
|
|
20
|
+
env_vars?: string[]
|
|
21
|
+
cwd?: string
|
|
22
|
+
url?: string
|
|
23
|
+
bearer_token_env_var?: string
|
|
24
|
+
http_headers?: Record<string, string>
|
|
25
|
+
env_http_headers?: Record<string, string>
|
|
26
|
+
enabled?: boolean
|
|
27
|
+
startup_timeout_sec?: number
|
|
28
|
+
startup_timeout_ms?: number
|
|
29
|
+
tool_timeout_sec?: number
|
|
30
|
+
enabled_tools?: string[]
|
|
31
|
+
disabled_tools?: string[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ProfileConfigSnapshot = {
|
|
35
|
+
path: string
|
|
36
|
+
codexHome: string
|
|
37
|
+
content: string
|
|
38
|
+
mcpServers: McpServerConfig[]
|
|
39
|
+
}
|
|
40
|
+
|
|
15
41
|
export type ThreadSearchResult = {
|
|
16
42
|
threadId: string
|
|
17
43
|
profileId: string
|
|
@@ -27,6 +53,13 @@ export type ThreadSearchResult = {
|
|
|
27
53
|
lastSeenAt: number | null
|
|
28
54
|
}
|
|
29
55
|
|
|
56
|
+
export type ActiveThread = {
|
|
57
|
+
threadId: string
|
|
58
|
+
profileId: string
|
|
59
|
+
turnId: string | null
|
|
60
|
+
startedAt: number
|
|
61
|
+
}
|
|
62
|
+
|
|
30
63
|
export type ReviewSessionResult = {
|
|
31
64
|
id: string
|
|
32
65
|
threadId: string
|
|
@@ -234,6 +267,40 @@ class HubClient {
|
|
|
234
267
|
return data.content ?? ''
|
|
235
268
|
}
|
|
236
269
|
|
|
270
|
+
async getProfileConfig(profileId: string): Promise<ProfileConfigSnapshot> {
|
|
271
|
+
const response = await fetch(`${HUB_URL}/profiles/${profileId}/config`)
|
|
272
|
+
if (!response.ok) {
|
|
273
|
+
throw new Error('Failed to load config')
|
|
274
|
+
}
|
|
275
|
+
return (await response.json()) as ProfileConfigSnapshot
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async saveProfileConfig(profileId: string, content: string): Promise<ProfileConfigSnapshot> {
|
|
279
|
+
const response = await fetch(`${HUB_URL}/profiles/${profileId}/config`, {
|
|
280
|
+
method: 'PUT',
|
|
281
|
+
headers: { 'Content-Type': 'application/json' },
|
|
282
|
+
body: JSON.stringify({ content }),
|
|
283
|
+
})
|
|
284
|
+
if (!response.ok) {
|
|
285
|
+
throw new Error('Failed to save config')
|
|
286
|
+
}
|
|
287
|
+
const data = (await response.json()) as { content?: string } & ProfileConfigSnapshot
|
|
288
|
+
return data
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async saveMcpServers(profileId: string, servers: McpServerConfig[]): Promise<ProfileConfigSnapshot> {
|
|
292
|
+
const response = await fetch(`${HUB_URL}/profiles/${profileId}/mcp-servers`, {
|
|
293
|
+
method: 'PUT',
|
|
294
|
+
headers: { 'Content-Type': 'application/json' },
|
|
295
|
+
body: JSON.stringify({ servers }),
|
|
296
|
+
})
|
|
297
|
+
if (!response.ok) {
|
|
298
|
+
throw new Error('Failed to save MCP servers')
|
|
299
|
+
}
|
|
300
|
+
const data = (await response.json()) as ProfileConfigSnapshot
|
|
301
|
+
return data
|
|
302
|
+
}
|
|
303
|
+
|
|
237
304
|
async searchThreads(params: {
|
|
238
305
|
query?: string
|
|
239
306
|
profileId?: string
|
|
@@ -261,6 +328,17 @@ class HubClient {
|
|
|
261
328
|
return data.threads ?? []
|
|
262
329
|
}
|
|
263
330
|
|
|
331
|
+
async listActiveThreads(params?: { profileId?: string }): Promise<ActiveThread[]> {
|
|
332
|
+
const url = new URL('/threads/active', HUB_URL)
|
|
333
|
+
if (params?.profileId) url.searchParams.set('profileId', params.profileId)
|
|
334
|
+
const response = await fetch(url.toString())
|
|
335
|
+
if (!response.ok) {
|
|
336
|
+
throw new Error('Failed to load active threads')
|
|
337
|
+
}
|
|
338
|
+
const data = (await response.json()) as { threads?: ActiveThread[] }
|
|
339
|
+
return data.threads ?? []
|
|
340
|
+
}
|
|
341
|
+
|
|
264
342
|
async listReviews(params?: { profileId?: string; limit?: number; offset?: number }): Promise<ReviewSessionResult[]> {
|
|
265
343
|
const url = new URL('/reviews', HUB_URL)
|
|
266
344
|
if (params?.profileId) url.searchParams.set('profileId', params.profileId)
|
|
@@ -274,7 +352,7 @@ class HubClient {
|
|
|
274
352
|
return data.sessions ?? []
|
|
275
353
|
}
|
|
276
354
|
|
|
277
|
-
async
|
|
355
|
+
private async sendRequest(profileId: string, method: string, params?: unknown): Promise<unknown> {
|
|
278
356
|
if (!this.ws) {
|
|
279
357
|
console.error('[HubClient] WebSocket not connected')
|
|
280
358
|
throw new Error('WebSocket not connected')
|
|
@@ -306,6 +384,18 @@ class HubClient {
|
|
|
306
384
|
})
|
|
307
385
|
}
|
|
308
386
|
|
|
387
|
+
async request(profileId: string, method: string, params?: unknown): Promise<unknown> {
|
|
388
|
+
try {
|
|
389
|
+
return await this.sendRequest(profileId, method, params)
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (this.isProfileNotRunning(error)) {
|
|
392
|
+
await this.startProfile(profileId)
|
|
393
|
+
return await this.sendRequest(profileId, method, params)
|
|
394
|
+
}
|
|
395
|
+
throw error
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
309
399
|
respond(profileId: string, id: number, result?: unknown, error?: { code?: number; message: string }) {
|
|
310
400
|
if (!this.ws) {
|
|
311
401
|
return
|
|
@@ -354,6 +444,13 @@ class HubClient {
|
|
|
354
444
|
|
|
355
445
|
this.listeners.forEach((listener) => listener(payload as WsEvent))
|
|
356
446
|
}
|
|
447
|
+
|
|
448
|
+
private isProfileNotRunning(error: unknown): boolean {
|
|
449
|
+
if (!error || typeof error !== 'object') {
|
|
450
|
+
return false
|
|
451
|
+
}
|
|
452
|
+
return error instanceof Error && error.message.includes('profile app-server not running')
|
|
453
|
+
}
|
|
357
454
|
}
|
|
358
455
|
|
|
359
456
|
export const hubClient = new HubClient()
|
|
@@ -20,6 +20,8 @@ interface AppState {
|
|
|
20
20
|
threadTurnStartedAt: Record<string, number>
|
|
21
21
|
threadLastTurnDuration: Record<string, number>
|
|
22
22
|
threadTokenUsage: Record<string, unknown>
|
|
23
|
+
threadPendingAccountSwitch: Record<string, { originalThreadId: string; previousAccountId: string }>
|
|
24
|
+
backendToUiThreadId: Record<string, string>
|
|
23
25
|
|
|
24
26
|
messages: Record<string, Message[]>
|
|
25
27
|
queuedMessages: Record<string, QueuedMessage[]>
|
|
@@ -59,6 +61,9 @@ interface AppState {
|
|
|
59
61
|
setThreadTurnStartedAt: (threadId: string, startedAt: number | null) => void
|
|
60
62
|
setThreadLastTurnDuration: (threadId: string, duration: number | null) => void
|
|
61
63
|
setThreadTokenUsage: (threadId: string, usage: unknown) => void
|
|
64
|
+
setThreadPendingAccountSwitch: (threadId: string, pending: { originalThreadId: string; previousAccountId: string } | null) => void
|
|
65
|
+
setBackendToUiThreadId: (backendThreadId: string, uiThreadId: string | null) => void
|
|
66
|
+
resolveThreadId: (threadId: string) => string
|
|
62
67
|
|
|
63
68
|
addMessage: (threadId: string, message: Message) => void
|
|
64
69
|
appendAgentDelta: (threadId: string, messageId: string, delta: string) => void
|
|
@@ -87,7 +92,7 @@ interface AppState {
|
|
|
87
92
|
closeMobileDrawers: () => void
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
export const useAppStore = create<AppState>((set) => ({
|
|
95
|
+
export const useAppStore = create<AppState>((set, get) => ({
|
|
91
96
|
accounts: [],
|
|
92
97
|
selectedAccountId: null,
|
|
93
98
|
connectionStatus: 'idle',
|
|
@@ -105,6 +110,8 @@ export const useAppStore = create<AppState>((set) => ({
|
|
|
105
110
|
threadTurnStartedAt: {},
|
|
106
111
|
threadLastTurnDuration: {},
|
|
107
112
|
threadTokenUsage: {},
|
|
113
|
+
threadPendingAccountSwitch: {},
|
|
114
|
+
backendToUiThreadId: {},
|
|
108
115
|
messages: {},
|
|
109
116
|
queuedMessages: {},
|
|
110
117
|
approvals: [],
|
|
@@ -325,6 +332,34 @@ export const useAppStore = create<AppState>((set) => ({
|
|
|
325
332
|
[threadId]: usage,
|
|
326
333
|
},
|
|
327
334
|
})),
|
|
335
|
+
setThreadPendingAccountSwitch: (threadId, pending) => set((state) => {
|
|
336
|
+
if (pending === null) {
|
|
337
|
+
const { [threadId]: _, ...rest } = state.threadPendingAccountSwitch
|
|
338
|
+
return { threadPendingAccountSwitch: rest }
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
threadPendingAccountSwitch: {
|
|
342
|
+
...state.threadPendingAccountSwitch,
|
|
343
|
+
[threadId]: pending,
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
}),
|
|
347
|
+
setBackendToUiThreadId: (backendThreadId, uiThreadId) => set((state) => {
|
|
348
|
+
if (uiThreadId === null) {
|
|
349
|
+
const { [backendThreadId]: _, ...rest } = state.backendToUiThreadId
|
|
350
|
+
return { backendToUiThreadId: rest }
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
backendToUiThreadId: {
|
|
354
|
+
...state.backendToUiThreadId,
|
|
355
|
+
[backendThreadId]: uiThreadId,
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
}),
|
|
359
|
+
resolveThreadId: (threadId: string): string => {
|
|
360
|
+
const state = get()
|
|
361
|
+
return state.backendToUiThreadId[threadId] ?? threadId
|
|
362
|
+
},
|
|
328
363
|
|
|
329
364
|
addMessage: (threadId, message) => set((state) => ({
|
|
330
365
|
messages: {
|
|
@@ -8,6 +8,29 @@ export type ReasoningSummary = 'auto' | 'concise' | 'detailed' | 'none'
|
|
|
8
8
|
export type ApprovalPolicy = 'untrusted' | 'on-failure' | 'on-request' | 'never'
|
|
9
9
|
export type ReviewStatus = 'pending' | 'running' | 'completed' | 'failed'
|
|
10
10
|
|
|
11
|
+
export type CommandAction =
|
|
12
|
+
| { type: 'read'; command: string; name: string; path: string }
|
|
13
|
+
| { type: 'listFiles'; command: string; path?: string | null }
|
|
14
|
+
| { type: 'search'; command: string; query?: string | null; path?: string | null }
|
|
15
|
+
| { type: 'unknown'; command: string }
|
|
16
|
+
|
|
17
|
+
export type FileChangeKind = 'add' | 'delete' | 'update'
|
|
18
|
+
|
|
19
|
+
export interface FileChangeMeta {
|
|
20
|
+
path: string
|
|
21
|
+
kind: FileChangeKind
|
|
22
|
+
diff?: string
|
|
23
|
+
movePath?: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MessageMeta {
|
|
27
|
+
commandActions?: CommandAction[]
|
|
28
|
+
command?: string
|
|
29
|
+
status?: string
|
|
30
|
+
fileChanges?: FileChangeMeta[]
|
|
31
|
+
diff?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
11
34
|
export interface Attachment {
|
|
12
35
|
id: string
|
|
13
36
|
type: 'image' | 'file'
|
|
@@ -60,6 +83,7 @@ export interface Thread {
|
|
|
60
83
|
createdAt: string
|
|
61
84
|
status: ThreadStatus
|
|
62
85
|
messageCount: number
|
|
86
|
+
backendThreadId?: string
|
|
63
87
|
}
|
|
64
88
|
|
|
65
89
|
export interface Message {
|
|
@@ -69,6 +93,7 @@ export interface Message {
|
|
|
69
93
|
kind?: MessageKind
|
|
70
94
|
title?: string
|
|
71
95
|
timestamp: string
|
|
96
|
+
meta?: MessageMeta
|
|
72
97
|
}
|
|
73
98
|
|
|
74
99
|
export interface QueuedMessage {
|