better-codex 0.1.4 → 0.2.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/apps/backend/README.md +43 -2
- package/apps/backend/src/core/app-server.ts +68 -2
- 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/settings-dialog.tsx +9 -1
- package/apps/web/src/components/layout/virtualized-message-list.tsx +581 -86
- package/apps/web/src/hooks/use-hub-connection.ts +21 -3
- 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/types/index.ts +24 -0
- package/apps/web/src/utils/item-format.ts +55 -9
- package/package.json +1 -1
|
@@ -278,8 +278,9 @@ export const useHubConnection = () => {
|
|
|
278
278
|
id: `diff-${turnId ?? threadId}`,
|
|
279
279
|
role: 'assistant',
|
|
280
280
|
kind: 'file',
|
|
281
|
-
title: 'Diff
|
|
281
|
+
title: 'Diff',
|
|
282
282
|
content,
|
|
283
|
+
meta: { diff },
|
|
283
284
|
timestamp: nowTimestamp(),
|
|
284
285
|
})
|
|
285
286
|
}
|
|
@@ -537,8 +538,25 @@ export const useHubConnection = () => {
|
|
|
537
538
|
})
|
|
538
539
|
if (threads) {
|
|
539
540
|
setThreadsForAccount(profile.id, toThreads(profile.id, threads))
|
|
540
|
-
|
|
541
|
-
|
|
541
|
+
}
|
|
542
|
+
if (threads?.data?.length) {
|
|
543
|
+
try {
|
|
544
|
+
const activeThreads = await hubClient.listActiveThreads({ profileId: profile.id })
|
|
545
|
+
const knownThreadIds = new Set(threads.data.map((thread) => thread.id))
|
|
546
|
+
for (const entry of activeThreads) {
|
|
547
|
+
if (!knownThreadIds.has(entry.threadId)) {
|
|
548
|
+
continue
|
|
549
|
+
}
|
|
550
|
+
updateThread(entry.threadId, { status: 'active' })
|
|
551
|
+
if (entry.turnId) {
|
|
552
|
+
setThreadTurnId(entry.threadId, entry.turnId)
|
|
553
|
+
}
|
|
554
|
+
if (Number.isFinite(entry.startedAt)) {
|
|
555
|
+
setThreadTurnStartedAt(entry.threadId, entry.startedAt)
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
} catch (error) {
|
|
559
|
+
console.error(error)
|
|
542
560
|
}
|
|
543
561
|
}
|
|
544
562
|
|
|
@@ -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()
|
|
@@ -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'
|
|
@@ -69,6 +92,7 @@ export interface Message {
|
|
|
69
92
|
kind?: MessageKind
|
|
70
93
|
title?: string
|
|
71
94
|
timestamp: string
|
|
95
|
+
meta?: MessageMeta
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
export interface QueuedMessage {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Message, MessageKind } from '../types'
|
|
1
|
+
import type { CommandAction, FileChangeMeta, Message, MessageKind, MessageMeta } from '../types'
|
|
2
2
|
|
|
3
3
|
type ThreadItem = {
|
|
4
4
|
type: string
|
|
@@ -6,11 +6,11 @@ type ThreadItem = {
|
|
|
6
6
|
[key: string]: unknown
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
type CommandAction =
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
// type CommandAction =
|
|
10
|
+
// | { type: 'read'; command: string; name: string; path: string }
|
|
11
|
+
// | { type: 'listFiles'; command: string; path?: string | null }
|
|
12
|
+
// | { type: 'search'; command: string; query?: string | null; path?: string | null }
|
|
13
|
+
// | { type: 'unknown'; command: string }
|
|
14
14
|
|
|
15
15
|
const clampText = (value: string, max = 1400) => {
|
|
16
16
|
if (value.length <= max) {
|
|
@@ -99,7 +99,43 @@ const formatCommandActions = (actions: CommandAction[], commandFallback: string)
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
type FormattedThreadItem = {
|
|
103
|
+
kind: MessageKind
|
|
104
|
+
content: string
|
|
105
|
+
title?: string
|
|
106
|
+
meta?: MessageMeta
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const toFileChangeMeta = (change: {
|
|
110
|
+
path?: unknown
|
|
111
|
+
kind?: unknown
|
|
112
|
+
diff?: string
|
|
113
|
+
movePath?: unknown
|
|
114
|
+
move_path?: unknown
|
|
115
|
+
}): FileChangeMeta => {
|
|
116
|
+
const path = typeof change.path === 'string' ? change.path : 'unknown'
|
|
117
|
+
const kind =
|
|
118
|
+
typeof change.kind === 'string'
|
|
119
|
+
? change.kind
|
|
120
|
+
: typeof (change.kind as { type?: string } | null | undefined)?.type === 'string'
|
|
121
|
+
? String((change.kind as { type?: string }).type)
|
|
122
|
+
: 'update'
|
|
123
|
+
const movePath =
|
|
124
|
+
typeof change.movePath === 'string'
|
|
125
|
+
? change.movePath
|
|
126
|
+
: typeof change.move_path === 'string'
|
|
127
|
+
? change.move_path
|
|
128
|
+
: undefined
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
path,
|
|
132
|
+
kind: kind === 'add' || kind === 'delete' ? kind : 'update',
|
|
133
|
+
diff: typeof change.diff === 'string' ? change.diff : undefined,
|
|
134
|
+
movePath: movePath ?? null,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const formatThreadItem = (item: ThreadItem): FormattedThreadItem | null => {
|
|
103
139
|
switch (item.type) {
|
|
104
140
|
case 'reasoning': {
|
|
105
141
|
const summary = Array.isArray(item.summary) ? item.summary.join('\n') : ''
|
|
@@ -113,13 +149,22 @@ export const formatThreadItem = (item: ThreadItem): { kind: MessageKind; content
|
|
|
113
149
|
const actions = Array.isArray(item.commandActions) ? (item.commandActions as CommandAction[]) : []
|
|
114
150
|
const actionSummary = formatCommandActions(actions, command)
|
|
115
151
|
const content = output ? `${actionSummary.content}\n\n${output}` : actionSummary.content
|
|
116
|
-
|
|
152
|
+
const meta: MessageMeta = {
|
|
153
|
+
commandActions: actions.length ? actions : undefined,
|
|
154
|
+
command,
|
|
155
|
+
status,
|
|
156
|
+
}
|
|
157
|
+
return { kind: actionSummary.kind, content: clampText(content), title: actionSummary.title, meta }
|
|
117
158
|
}
|
|
118
159
|
case 'fileChange': {
|
|
119
160
|
const changes = Array.isArray(item.changes) ? item.changes : []
|
|
120
161
|
const content = formatFileChanges(changes as Array<{ path?: string; kind?: string; diff?: string; movePath?: string }>)
|
|
121
162
|
const status = typeof item.status === 'string' ? item.status : 'inProgress'
|
|
122
|
-
|
|
163
|
+
const meta: MessageMeta = {
|
|
164
|
+
fileChanges: changes.length ? changes.map((change) => toFileChangeMeta(change as Record<string, unknown>)) : undefined,
|
|
165
|
+
status,
|
|
166
|
+
}
|
|
167
|
+
return { kind: 'file', content, title: 'Files', meta }
|
|
123
168
|
}
|
|
124
169
|
case 'mcpToolCall': {
|
|
125
170
|
const server = typeof item.server === 'string' ? item.server : 'mcp'
|
|
@@ -166,5 +211,6 @@ export const buildSystemMessage = (item: ThreadItem): Message | null => {
|
|
|
166
211
|
title: formatted.title,
|
|
167
212
|
content: formatted.content,
|
|
168
213
|
timestamp: '',
|
|
214
|
+
meta: formatted.meta,
|
|
169
215
|
}
|
|
170
216
|
}
|