@codori/client 0.0.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/README.md +16 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +7 -0
- package/app/assets/css/main.css +75 -0
- package/app/components/ChatWorkspace.vue +1499 -0
- package/app/components/MessageContent.vue +27 -0
- package/app/components/MessagePartRenderer.ts +55 -0
- package/app/components/ProjectSidebar.vue +146 -0
- package/app/components/ProjectStatusDot.vue +76 -0
- package/app/components/ThreadList.vue +165 -0
- package/app/components/ThreadPanel.vue +94 -0
- package/app/components/TunnelNotice.vue +27 -0
- package/app/components/VisualSubagentStack.vue +267 -0
- package/app/components/message-item/CommandExecution.vue +66 -0
- package/app/components/message-item/ContextCompaction.vue +9 -0
- package/app/components/message-item/DynamicToolCall.vue +108 -0
- package/app/components/message-item/FileChange.vue +114 -0
- package/app/components/message-item/McpToolCall.vue +86 -0
- package/app/components/message-item/SubagentActivity.vue +181 -0
- package/app/components/message-item/UnifiedDiffViewer.vue +190 -0
- package/app/components/message-item/WebSearch.vue +24 -0
- package/app/components/message-item/use-chat-tool-state.ts +27 -0
- package/app/components/message-part/Event.vue +57 -0
- package/app/components/message-part/Item.ts +50 -0
- package/app/components/message-part/Text.vue +35 -0
- package/app/composables/useChatSession.ts +59 -0
- package/app/composables/useChatSubmitGuard.ts +28 -0
- package/app/composables/useProjects.ts +102 -0
- package/app/composables/useRpc.ts +34 -0
- package/app/composables/useThreadPanel.ts +18 -0
- package/app/composables/useVisualSubagentPanels.ts +20 -0
- package/app/layouts/default.vue +81 -0
- package/app/pages/index.vue +62 -0
- package/app/pages/projects/[...projectId]/index.vue +104 -0
- package/app/pages/projects/[...projectId]/threads/[threadId].vue +303 -0
- package/nuxt.config.ts +19 -0
- package/package.json +50 -0
- package/server/api/codori/projects/[projectId]/start.post.ts +17 -0
- package/server/api/codori/projects/[projectId]/status.get.ts +16 -0
- package/server/api/codori/projects/[projectId]/stop.post.ts +17 -0
- package/server/api/codori/projects/[projectId].get.ts +13 -0
- package/server/api/codori/projects/index.get.ts +7 -0
- package/server/utils/server-proxy.ts +25 -0
- package/shared/codex-chat.ts +340 -0
- package/shared/codex-rpc.ts +467 -0
- package/shared/codori.ts +62 -0
- package/shared/network.ts +34 -0
|
@@ -0,0 +1,1499 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useRouter } from '#imports'
|
|
3
|
+
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
4
|
+
import MessageContent from './MessageContent.vue'
|
|
5
|
+
import { useChatSession, type SubagentPanelState } from '../composables/useChatSession.js'
|
|
6
|
+
import { useProjects } from '../composables/useProjects.js'
|
|
7
|
+
import { useRpc } from '../composables/useRpc.js'
|
|
8
|
+
import { useChatSubmitGuard } from '../composables/useChatSubmitGuard.js'
|
|
9
|
+
import {
|
|
10
|
+
ITEM_PART,
|
|
11
|
+
eventToMessage,
|
|
12
|
+
isSubagentActiveStatus,
|
|
13
|
+
itemToMessages,
|
|
14
|
+
threadToMessages,
|
|
15
|
+
upsertStreamingMessage,
|
|
16
|
+
type ChatMessage,
|
|
17
|
+
type ChatPart,
|
|
18
|
+
type FileChangeItem,
|
|
19
|
+
type ItemData,
|
|
20
|
+
type McpToolCallItem
|
|
21
|
+
} from '~~/shared/codex-chat.js'
|
|
22
|
+
import {
|
|
23
|
+
notificationThreadId,
|
|
24
|
+
notificationTurnId,
|
|
25
|
+
type CodexRpcNotification,
|
|
26
|
+
type CodexThread,
|
|
27
|
+
type CodexThreadItem,
|
|
28
|
+
type ThreadReadResponse,
|
|
29
|
+
type ThreadResumeResponse,
|
|
30
|
+
type ThreadStartResponse,
|
|
31
|
+
type TurnStartResponse
|
|
32
|
+
} from '~~/shared/codex-rpc.js'
|
|
33
|
+
import { toProjectThreadRoute } from '~~/shared/codori.js'
|
|
34
|
+
|
|
35
|
+
const props = defineProps<{
|
|
36
|
+
projectId: string
|
|
37
|
+
threadId?: string | null
|
|
38
|
+
}>()
|
|
39
|
+
|
|
40
|
+
const router = useRouter()
|
|
41
|
+
const { getClient } = useRpc()
|
|
42
|
+
const {
|
|
43
|
+
loaded,
|
|
44
|
+
refreshProjects,
|
|
45
|
+
getProject,
|
|
46
|
+
startProject
|
|
47
|
+
} = useProjects()
|
|
48
|
+
const {
|
|
49
|
+
onCompositionStart,
|
|
50
|
+
onCompositionEnd,
|
|
51
|
+
shouldSubmit
|
|
52
|
+
} = useChatSubmitGuard()
|
|
53
|
+
|
|
54
|
+
const input = ref('')
|
|
55
|
+
const scrollViewport = ref<HTMLElement | null>(null)
|
|
56
|
+
const pinnedToBottom = ref(true)
|
|
57
|
+
const session = useChatSession(props.projectId)
|
|
58
|
+
const {
|
|
59
|
+
messages,
|
|
60
|
+
subagentPanels,
|
|
61
|
+
status,
|
|
62
|
+
error,
|
|
63
|
+
activeThreadId,
|
|
64
|
+
threadTitle,
|
|
65
|
+
pendingThreadId,
|
|
66
|
+
autoRedirectThreadId,
|
|
67
|
+
loadVersion
|
|
68
|
+
} = session
|
|
69
|
+
|
|
70
|
+
const selectedProject = computed(() => getProject(props.projectId))
|
|
71
|
+
const submitError = computed(() => error.value ? new Error(error.value) : undefined)
|
|
72
|
+
const isBusy = computed(() => status.value === 'submitted' || status.value === 'streaming')
|
|
73
|
+
const routeThreadId = computed(() => props.threadId ?? null)
|
|
74
|
+
const projectTitle = computed(() => selectedProject.value?.projectId ?? props.projectId)
|
|
75
|
+
const showWelcomeState = computed(() =>
|
|
76
|
+
!routeThreadId.value
|
|
77
|
+
&& !activeThreadId.value
|
|
78
|
+
&& messages.value.length === 0
|
|
79
|
+
&& !isBusy.value
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const starterPrompts = computed(() => {
|
|
83
|
+
const project = projectTitle.value
|
|
84
|
+
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
title: 'Map the codebase',
|
|
88
|
+
text: `Summarize the structure of ${project} and identify the main entry points.`
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
title: 'Find the next task',
|
|
92
|
+
text: `Inspect ${project} and suggest the highest-impact improvement to make next.`
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
title: 'Build a plan',
|
|
96
|
+
text: `Read ${project} and propose a concrete implementation plan for the next feature.`
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const subagentBootstrapPromises = new Map<string, Promise<void>>()
|
|
102
|
+
|
|
103
|
+
const isActiveTurnStatus = (value: string | null | undefined) => {
|
|
104
|
+
if (!value) {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return !/(completed|failed|error|cancelled|canceled|interrupted|stopped)/i.test(value)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const currentLiveStream = () =>
|
|
112
|
+
session.liveStream?.threadId === activeThreadId.value
|
|
113
|
+
? session.liveStream
|
|
114
|
+
: null
|
|
115
|
+
|
|
116
|
+
const clearLiveStream = () => {
|
|
117
|
+
session.liveStream?.unsubscribe?.()
|
|
118
|
+
session.liveStream = null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const stripOptimisticDraftMessages = () => {
|
|
122
|
+
messages.value = messages.value.filter(message => !message.id.startsWith('local-user-'))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const resolveThreadTitle = (thread: { name: string | null, preview: string, id: string }) => {
|
|
126
|
+
const nextTitle = thread.name?.trim() || thread.preview.trim()
|
|
127
|
+
return nextTitle || `Thread ${thread.id}`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const shortThreadId = (value: string) => value.slice(0, 8)
|
|
131
|
+
|
|
132
|
+
const resolveSubagentName = (
|
|
133
|
+
threadId: string,
|
|
134
|
+
thread?: Pick<CodexThread, 'agentNickname' | 'name' | 'preview'> | null
|
|
135
|
+
) => {
|
|
136
|
+
const candidate = thread?.agentNickname?.trim()
|
|
137
|
+
|| thread?.name?.trim()
|
|
138
|
+
|| thread?.preview?.trim()
|
|
139
|
+
|
|
140
|
+
if (candidate) {
|
|
141
|
+
return candidate
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return `Agent ${shortThreadId(threadId)}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const getSubagentPanel = (threadId: string) =>
|
|
148
|
+
subagentPanels.value.find(panel => panel.threadId === threadId)
|
|
149
|
+
|
|
150
|
+
const createSubagentPanelState = (threadId: string): SubagentPanelState => ({
|
|
151
|
+
threadId,
|
|
152
|
+
name: resolveSubagentName(threadId),
|
|
153
|
+
status: null,
|
|
154
|
+
messages: [],
|
|
155
|
+
firstSeenAt: Date.now(),
|
|
156
|
+
lastSeenAt: Date.now(),
|
|
157
|
+
turnId: null,
|
|
158
|
+
bootstrapped: false,
|
|
159
|
+
bufferedNotifications: []
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const upsertSubagentPanel = (
|
|
163
|
+
threadId: string,
|
|
164
|
+
updater: (panel: SubagentPanelState | undefined) => SubagentPanelState
|
|
165
|
+
) => {
|
|
166
|
+
const current = getSubagentPanel(threadId)
|
|
167
|
+
const nextPanel = updater(current)
|
|
168
|
+
const index = subagentPanels.value.findIndex(panel => panel.threadId === threadId)
|
|
169
|
+
|
|
170
|
+
if (index === -1) {
|
|
171
|
+
subagentPanels.value = [...subagentPanels.value, nextPanel]
|
|
172
|
+
return nextPanel
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
subagentPanels.value = subagentPanels.value.map((panel, panelIndex) =>
|
|
176
|
+
panelIndex === index ? nextPanel : panel
|
|
177
|
+
)
|
|
178
|
+
return nextPanel
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const rememberObservedSubagentThread = (threadId: string) => {
|
|
182
|
+
session.liveStream?.observedSubagentThreadIds.add(threadId)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const isTextPart = (part: ChatPart): part is Extract<ChatPart, { type: 'text' }> =>
|
|
186
|
+
part.type === 'text'
|
|
187
|
+
|
|
188
|
+
const isItemPart = (part: ChatPart): part is Extract<ChatPart, { type: typeof ITEM_PART }> =>
|
|
189
|
+
part.type === ITEM_PART
|
|
190
|
+
|
|
191
|
+
const getFallbackItemData = (message: ChatMessage) => {
|
|
192
|
+
const itemPart = message.parts.find(isItemPart)
|
|
193
|
+
if (!itemPart) {
|
|
194
|
+
throw new Error('Expected fallback item part.')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return itemPart.data
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const updatePinnedState = () => {
|
|
201
|
+
const viewport = scrollViewport.value
|
|
202
|
+
if (!viewport) {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
pinnedToBottom.value = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight < 80
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const scrollToBottom = (behavior: ScrollBehavior = 'auto') => {
|
|
210
|
+
const viewport = scrollViewport.value
|
|
211
|
+
if (!viewport) {
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
viewport.scrollTo({
|
|
216
|
+
top: viewport.scrollHeight,
|
|
217
|
+
behavior
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const scheduleScrollToBottom = async (behavior: ScrollBehavior = 'auto') => {
|
|
222
|
+
await nextTick()
|
|
223
|
+
|
|
224
|
+
if (!import.meta.client) {
|
|
225
|
+
scrollToBottom(behavior)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
requestAnimationFrame(() => {
|
|
230
|
+
if (pinnedToBottom.value || isBusy.value) {
|
|
231
|
+
scrollToBottom(behavior)
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const ensureProjectRuntime = async () => {
|
|
237
|
+
if (!loaded.value) {
|
|
238
|
+
await refreshProjects()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (selectedProject.value?.status === 'running') {
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
await startProject(props.projectId)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const hydrateThread = async (threadId: string) => {
|
|
249
|
+
const requestVersion = loadVersion.value + 1
|
|
250
|
+
loadVersion.value = requestVersion
|
|
251
|
+
error.value = null
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
await ensureProjectRuntime()
|
|
255
|
+
const client = getClient(props.projectId)
|
|
256
|
+
activeThreadId.value = threadId
|
|
257
|
+
|
|
258
|
+
const existingLiveStream = currentLiveStream()
|
|
259
|
+
|
|
260
|
+
if (!existingLiveStream) {
|
|
261
|
+
const nextLiveStream = {
|
|
262
|
+
threadId,
|
|
263
|
+
turnId: null,
|
|
264
|
+
bufferedNotifications: [] as CodexRpcNotification[],
|
|
265
|
+
observedSubagentThreadIds: new Set<string>(),
|
|
266
|
+
unsubscribe: null as (() => void) | null
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
nextLiveStream.unsubscribe = client.subscribe((notification) => {
|
|
270
|
+
const targetThreadId = notificationThreadId(notification)
|
|
271
|
+
if (!targetThreadId) {
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (targetThreadId !== threadId) {
|
|
276
|
+
if (nextLiveStream.observedSubagentThreadIds.has(targetThreadId)) {
|
|
277
|
+
applySubagentNotification(targetThreadId, notification)
|
|
278
|
+
}
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!nextLiveStream.turnId) {
|
|
283
|
+
nextLiveStream.bufferedNotifications.push(notification)
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const turnId = notificationTurnId(notification)
|
|
288
|
+
if (turnId && turnId !== nextLiveStream.turnId) {
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
applyNotification(notification)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
session.liveStream = nextLiveStream
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
await client.request<ThreadResumeResponse>('thread/resume', {
|
|
299
|
+
threadId,
|
|
300
|
+
cwd: selectedProject.value?.projectPath ?? null,
|
|
301
|
+
approvalPolicy: 'never',
|
|
302
|
+
persistExtendedHistory: true
|
|
303
|
+
})
|
|
304
|
+
const response = await client.request<ThreadReadResponse>('thread/read', {
|
|
305
|
+
threadId,
|
|
306
|
+
includeTurns: true
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
if (loadVersion.value !== requestVersion) {
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
activeThreadId.value = response.thread.id
|
|
314
|
+
threadTitle.value = resolveThreadTitle(response.thread)
|
|
315
|
+
messages.value = threadToMessages(response.thread)
|
|
316
|
+
rebuildSubagentPanelsFromThread(response.thread)
|
|
317
|
+
const activeTurn = [...response.thread.turns].reverse().find(turn => isActiveTurnStatus(turn.status))
|
|
318
|
+
|
|
319
|
+
if (!activeTurn) {
|
|
320
|
+
clearLiveStream()
|
|
321
|
+
status.value = 'ready'
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!session.liveStream) {
|
|
326
|
+
status.value = 'streaming'
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
session.liveStream.turnId = activeTurn.id
|
|
331
|
+
status.value = 'streaming'
|
|
332
|
+
|
|
333
|
+
const pendingNotifications = session.liveStream.bufferedNotifications.splice(0, session.liveStream.bufferedNotifications.length)
|
|
334
|
+
for (const notification of pendingNotifications) {
|
|
335
|
+
const turnId = notificationTurnId(notification)
|
|
336
|
+
if (turnId && turnId !== activeTurn.id) {
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
applyNotification(notification)
|
|
341
|
+
}
|
|
342
|
+
} catch (caughtError) {
|
|
343
|
+
clearLiveStream()
|
|
344
|
+
error.value = caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
345
|
+
status.value = 'error'
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const resetDraftThread = () => {
|
|
350
|
+
clearLiveStream()
|
|
351
|
+
activeThreadId.value = null
|
|
352
|
+
threadTitle.value = null
|
|
353
|
+
pendingThreadId.value = null
|
|
354
|
+
autoRedirectThreadId.value = null
|
|
355
|
+
messages.value = []
|
|
356
|
+
subagentPanels.value = []
|
|
357
|
+
error.value = null
|
|
358
|
+
status.value = 'ready'
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const ensureThread = async () => {
|
|
362
|
+
if (activeThreadId.value) {
|
|
363
|
+
return {
|
|
364
|
+
threadId: activeThreadId.value,
|
|
365
|
+
created: false
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
await ensureProjectRuntime()
|
|
370
|
+
const client = getClient(props.projectId)
|
|
371
|
+
const response = await client.request<ThreadStartResponse>('thread/start', {
|
|
372
|
+
cwd: selectedProject.value?.projectPath ?? null,
|
|
373
|
+
approvalPolicy: 'never',
|
|
374
|
+
experimentalRawEvents: false,
|
|
375
|
+
persistExtendedHistory: true
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
activeThreadId.value = response.thread.id
|
|
379
|
+
threadTitle.value = resolveThreadTitle(response.thread)
|
|
380
|
+
return {
|
|
381
|
+
threadId: response.thread.id,
|
|
382
|
+
created: true
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const seedStreamingMessage = (item: CodexThreadItem) => {
|
|
387
|
+
const [seed] = itemToMessages(item)
|
|
388
|
+
if (!seed) {
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
messages.value = upsertStreamingMessage(messages.value, {
|
|
393
|
+
...seed,
|
|
394
|
+
pending: true
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const updateMessage = (
|
|
399
|
+
messageId: string,
|
|
400
|
+
fallbackMessage: ChatMessage,
|
|
401
|
+
transform: (message: ChatMessage) => ChatMessage
|
|
402
|
+
) => {
|
|
403
|
+
const existing = messages.value.find(message => message.id === messageId)
|
|
404
|
+
messages.value = upsertStreamingMessage(messages.value, transform(existing ?? fallbackMessage))
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const appendTextPartDelta = (
|
|
408
|
+
messageId: string,
|
|
409
|
+
delta: string,
|
|
410
|
+
fallbackMessage: ChatMessage
|
|
411
|
+
) => {
|
|
412
|
+
updateMessage(messageId, fallbackMessage, (message) => {
|
|
413
|
+
const partIndex = message.parts.findIndex(isTextPart)
|
|
414
|
+
const existingTextPart = partIndex === -1 ? null : message.parts[partIndex] as Extract<ChatPart, { type: 'text' }>
|
|
415
|
+
const nextText = existingTextPart ? `${existingTextPart.text}${delta}` : delta
|
|
416
|
+
const nextTextPart: Extract<ChatPart, { type: 'text' }> = {
|
|
417
|
+
type: 'text',
|
|
418
|
+
text: nextText,
|
|
419
|
+
state: 'streaming'
|
|
420
|
+
}
|
|
421
|
+
const nextParts = partIndex === -1
|
|
422
|
+
? [...message.parts, nextTextPart]
|
|
423
|
+
: message.parts.map((part, index) => index === partIndex ? nextTextPart : part)
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
...message,
|
|
427
|
+
pending: true,
|
|
428
|
+
parts: nextParts
|
|
429
|
+
}
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const updateItemPart = (
|
|
434
|
+
messageId: string,
|
|
435
|
+
fallbackMessage: ChatMessage,
|
|
436
|
+
transform: (itemData: ItemData) => ItemData
|
|
437
|
+
) => {
|
|
438
|
+
updateMessage(messageId, fallbackMessage, (message) => {
|
|
439
|
+
const partIndex = message.parts.findIndex(isItemPart)
|
|
440
|
+
const existingData = partIndex === -1 ? null : (message.parts[partIndex] as Extract<ChatPart, { type: typeof ITEM_PART }>).data
|
|
441
|
+
const nextData = transform(existingData ?? getFallbackItemData(fallbackMessage))
|
|
442
|
+
const nextPart: Extract<ChatPart, { type: typeof ITEM_PART }> = {
|
|
443
|
+
type: ITEM_PART,
|
|
444
|
+
data: nextData
|
|
445
|
+
}
|
|
446
|
+
const nextParts = partIndex === -1
|
|
447
|
+
? [...message.parts, nextPart]
|
|
448
|
+
: message.parts.map((part, index) => index === partIndex ? nextPart : part)
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
...message,
|
|
452
|
+
pending: true,
|
|
453
|
+
parts: nextParts
|
|
454
|
+
}
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const fallbackCommandMessage = (itemId: string): ChatMessage => ({
|
|
459
|
+
id: itemId,
|
|
460
|
+
role: 'system',
|
|
461
|
+
pending: true,
|
|
462
|
+
parts: [{
|
|
463
|
+
type: ITEM_PART,
|
|
464
|
+
data: {
|
|
465
|
+
kind: 'command_execution',
|
|
466
|
+
item: {
|
|
467
|
+
type: 'commandExecution',
|
|
468
|
+
id: itemId,
|
|
469
|
+
command: 'Command',
|
|
470
|
+
aggregatedOutput: '',
|
|
471
|
+
exitCode: null,
|
|
472
|
+
status: 'inProgress'
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}]
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
const fallbackFileChangeMessage = (itemId: string): ChatMessage => ({
|
|
479
|
+
id: itemId,
|
|
480
|
+
role: 'system',
|
|
481
|
+
pending: true,
|
|
482
|
+
parts: [{
|
|
483
|
+
type: ITEM_PART,
|
|
484
|
+
data: {
|
|
485
|
+
kind: 'file_change',
|
|
486
|
+
item: {
|
|
487
|
+
type: 'fileChange',
|
|
488
|
+
id: itemId,
|
|
489
|
+
changes: [],
|
|
490
|
+
status: 'inProgress',
|
|
491
|
+
liveOutput: ''
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}]
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
const fallbackMcpToolMessage = (itemId: string): ChatMessage => ({
|
|
498
|
+
id: itemId,
|
|
499
|
+
role: 'system',
|
|
500
|
+
pending: true,
|
|
501
|
+
parts: [{
|
|
502
|
+
type: ITEM_PART,
|
|
503
|
+
data: {
|
|
504
|
+
kind: 'mcp_tool_call',
|
|
505
|
+
item: {
|
|
506
|
+
type: 'mcpToolCall',
|
|
507
|
+
id: itemId,
|
|
508
|
+
server: 'mcp',
|
|
509
|
+
tool: 'tool',
|
|
510
|
+
arguments: null,
|
|
511
|
+
result: null,
|
|
512
|
+
error: null,
|
|
513
|
+
status: 'inProgress',
|
|
514
|
+
progressMessages: []
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}]
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
const fallbackCommandItemData = (itemId: string) =>
|
|
521
|
+
getFallbackItemData(fallbackCommandMessage(itemId)) as Extract<ItemData, { kind: 'command_execution' }>
|
|
522
|
+
|
|
523
|
+
const fallbackFileChangeItemData = (itemId: string) =>
|
|
524
|
+
getFallbackItemData(fallbackFileChangeMessage(itemId)) as Extract<ItemData, { kind: 'file_change' }>
|
|
525
|
+
|
|
526
|
+
const fallbackMcpToolItemData = (itemId: string) =>
|
|
527
|
+
getFallbackItemData(fallbackMcpToolMessage(itemId)) as Extract<ItemData, { kind: 'mcp_tool_call' }>
|
|
528
|
+
|
|
529
|
+
const pushEventMessage = (kind: 'turn.failed' | 'stream.error', messageText: string) => {
|
|
530
|
+
messages.value = upsertStreamingMessage(
|
|
531
|
+
messages.value,
|
|
532
|
+
eventToMessage(`event-${kind}-${Date.now()}`, kind === 'turn.failed'
|
|
533
|
+
? {
|
|
534
|
+
kind,
|
|
535
|
+
error: {
|
|
536
|
+
message: messageText
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
: {
|
|
540
|
+
kind,
|
|
541
|
+
message: messageText
|
|
542
|
+
})
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const updateSubagentPanelMessages = (
|
|
547
|
+
threadId: string,
|
|
548
|
+
updater: (panelMessages: ChatMessage[]) => ChatMessage[]
|
|
549
|
+
) => {
|
|
550
|
+
upsertSubagentPanel(threadId, (panel) => {
|
|
551
|
+
const basePanel = panel ?? createSubagentPanelState(threadId)
|
|
552
|
+
return {
|
|
553
|
+
...basePanel,
|
|
554
|
+
messages: updater(basePanel.messages),
|
|
555
|
+
lastSeenAt: Date.now()
|
|
556
|
+
}
|
|
557
|
+
})
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const updateSubagentMessage = (
|
|
561
|
+
threadId: string,
|
|
562
|
+
messageId: string,
|
|
563
|
+
fallbackMessage: ChatMessage,
|
|
564
|
+
transform: (message: ChatMessage) => ChatMessage
|
|
565
|
+
) => {
|
|
566
|
+
updateSubagentPanelMessages(threadId, (panelMessages) => {
|
|
567
|
+
const existing = panelMessages.find(message => message.id === messageId)
|
|
568
|
+
return upsertStreamingMessage(panelMessages, transform(existing ?? fallbackMessage))
|
|
569
|
+
})
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const appendSubagentTextPartDelta = (
|
|
573
|
+
threadId: string,
|
|
574
|
+
messageId: string,
|
|
575
|
+
delta: string,
|
|
576
|
+
fallbackMessage: ChatMessage
|
|
577
|
+
) => {
|
|
578
|
+
updateSubagentMessage(threadId, messageId, fallbackMessage, (message) => {
|
|
579
|
+
const partIndex = message.parts.findIndex(isTextPart)
|
|
580
|
+
const existingTextPart = partIndex === -1 ? null : message.parts[partIndex] as Extract<ChatPart, { type: 'text' }>
|
|
581
|
+
const nextText = existingTextPart ? `${existingTextPart.text}${delta}` : delta
|
|
582
|
+
const nextTextPart: Extract<ChatPart, { type: 'text' }> = {
|
|
583
|
+
type: 'text',
|
|
584
|
+
text: nextText,
|
|
585
|
+
state: 'streaming'
|
|
586
|
+
}
|
|
587
|
+
const nextParts = partIndex === -1
|
|
588
|
+
? [...message.parts, nextTextPart]
|
|
589
|
+
: message.parts.map((part, index) => index === partIndex ? nextTextPart : part)
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
...message,
|
|
593
|
+
pending: true,
|
|
594
|
+
parts: nextParts
|
|
595
|
+
}
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const updateSubagentItemPart = (
|
|
600
|
+
threadId: string,
|
|
601
|
+
messageId: string,
|
|
602
|
+
fallbackMessage: ChatMessage,
|
|
603
|
+
transform: (itemData: ItemData) => ItemData
|
|
604
|
+
) => {
|
|
605
|
+
updateSubagentMessage(threadId, messageId, fallbackMessage, (message) => {
|
|
606
|
+
const partIndex = message.parts.findIndex(isItemPart)
|
|
607
|
+
const existingData = partIndex === -1 ? null : (message.parts[partIndex] as Extract<ChatPart, { type: typeof ITEM_PART }>).data
|
|
608
|
+
const nextData = transform(existingData ?? getFallbackItemData(fallbackMessage))
|
|
609
|
+
const nextPart: Extract<ChatPart, { type: typeof ITEM_PART }> = {
|
|
610
|
+
type: ITEM_PART,
|
|
611
|
+
data: nextData
|
|
612
|
+
}
|
|
613
|
+
const nextParts = partIndex === -1
|
|
614
|
+
? [...message.parts, nextPart]
|
|
615
|
+
: message.parts.map((part, index) => index === partIndex ? nextPart : part)
|
|
616
|
+
|
|
617
|
+
return {
|
|
618
|
+
...message,
|
|
619
|
+
pending: true,
|
|
620
|
+
parts: nextParts
|
|
621
|
+
}
|
|
622
|
+
})
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const pushSubagentEventMessage = (threadId: string, kind: 'turn.failed' | 'stream.error', messageText: string) => {
|
|
626
|
+
updateSubagentPanelMessages(threadId, (panelMessages) =>
|
|
627
|
+
upsertStreamingMessage(
|
|
628
|
+
panelMessages,
|
|
629
|
+
eventToMessage(`subagent-event-${threadId}-${kind}-${Date.now()}`, kind === 'turn.failed'
|
|
630
|
+
? {
|
|
631
|
+
kind,
|
|
632
|
+
error: {
|
|
633
|
+
message: messageText
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
: {
|
|
637
|
+
kind,
|
|
638
|
+
message: messageText
|
|
639
|
+
})
|
|
640
|
+
)
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const seedSubagentStreamingMessage = (threadId: string, item: CodexThreadItem) => {
|
|
645
|
+
const [seed] = itemToMessages(item)
|
|
646
|
+
if (!seed) {
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
updateSubagentPanelMessages(threadId, (panelMessages) =>
|
|
651
|
+
upsertStreamingMessage(panelMessages, {
|
|
652
|
+
...seed,
|
|
653
|
+
pending: true
|
|
654
|
+
})
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const bootstrapSubagentPanel = async (threadId: string) => {
|
|
659
|
+
if (!threadId) {
|
|
660
|
+
return
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const existingPromise = subagentBootstrapPromises.get(threadId)
|
|
664
|
+
if (existingPromise) {
|
|
665
|
+
await existingPromise
|
|
666
|
+
return
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const bootstrapPromise = (async () => {
|
|
670
|
+
try {
|
|
671
|
+
await ensureProjectRuntime()
|
|
672
|
+
const client = getClient(props.projectId)
|
|
673
|
+
const response = await client.request<ThreadReadResponse>('thread/read', {
|
|
674
|
+
threadId,
|
|
675
|
+
includeTurns: true
|
|
676
|
+
})
|
|
677
|
+
const activeTurn = [...response.thread.turns].reverse().find(turn => isActiveTurnStatus(turn.status))
|
|
678
|
+
const pendingNotifications = getSubagentPanel(threadId)?.bufferedNotifications.slice() ?? []
|
|
679
|
+
|
|
680
|
+
upsertSubagentPanel(threadId, (panel) => {
|
|
681
|
+
const basePanel = panel ?? createSubagentPanelState(threadId)
|
|
682
|
+
return {
|
|
683
|
+
...basePanel,
|
|
684
|
+
name: resolveSubagentName(threadId, response.thread),
|
|
685
|
+
messages: threadToMessages(response.thread),
|
|
686
|
+
turnId: activeTurn?.id ?? null,
|
|
687
|
+
bootstrapped: true,
|
|
688
|
+
bufferedNotifications: [],
|
|
689
|
+
status: panel?.status
|
|
690
|
+
?? (activeTurn ? 'running' : basePanel.status)
|
|
691
|
+
?? null,
|
|
692
|
+
lastSeenAt: Date.now()
|
|
693
|
+
}
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
for (const notification of pendingNotifications) {
|
|
697
|
+
applySubagentNotification(threadId, notification)
|
|
698
|
+
}
|
|
699
|
+
} catch {
|
|
700
|
+
upsertSubagentPanel(threadId, (panel) => ({
|
|
701
|
+
...(panel ?? createSubagentPanelState(threadId)),
|
|
702
|
+
lastSeenAt: Date.now()
|
|
703
|
+
}))
|
|
704
|
+
} finally {
|
|
705
|
+
subagentBootstrapPromises.delete(threadId)
|
|
706
|
+
}
|
|
707
|
+
})()
|
|
708
|
+
|
|
709
|
+
subagentBootstrapPromises.set(threadId, bootstrapPromise)
|
|
710
|
+
await bootstrapPromise
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const applySubagentActivityItem = (item: Extract<CodexThreadItem, { type: 'collabAgentToolCall' }>) => {
|
|
714
|
+
const orderedThreadIds = [
|
|
715
|
+
...item.receiverThreadIds,
|
|
716
|
+
...Object.keys(item.agentsStates).filter(threadId => !item.receiverThreadIds.includes(threadId))
|
|
717
|
+
]
|
|
718
|
+
|
|
719
|
+
for (const threadId of orderedThreadIds) {
|
|
720
|
+
const agentState = item.agentsStates[threadId]
|
|
721
|
+
rememberObservedSubagentThread(threadId)
|
|
722
|
+
upsertSubagentPanel(threadId, (panel) => {
|
|
723
|
+
const basePanel = panel ?? createSubagentPanelState(threadId)
|
|
724
|
+
return {
|
|
725
|
+
...basePanel,
|
|
726
|
+
status: agentState?.status ?? basePanel.status,
|
|
727
|
+
lastSeenAt: Date.now()
|
|
728
|
+
}
|
|
729
|
+
})
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
for (const threadId of orderedThreadIds) {
|
|
733
|
+
void bootstrapSubagentPanel(threadId)
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const rebuildSubagentPanelsFromThread = (thread: CodexThread) => {
|
|
738
|
+
subagentPanels.value = []
|
|
739
|
+
for (const turn of thread.turns) {
|
|
740
|
+
for (const item of turn.items) {
|
|
741
|
+
if (item.type === 'collabAgentToolCall') {
|
|
742
|
+
applySubagentActivityItem(item)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const applySubagentNotification = (threadId: string, notification: CodexRpcNotification) => {
|
|
749
|
+
const panel = getSubagentPanel(threadId)
|
|
750
|
+
if (!panel) {
|
|
751
|
+
return
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (!panel.bootstrapped) {
|
|
755
|
+
upsertSubagentPanel(threadId, (existingPanel) => ({
|
|
756
|
+
...(existingPanel ?? createSubagentPanelState(threadId)),
|
|
757
|
+
bufferedNotifications: [...(existingPanel?.bufferedNotifications ?? []), notification],
|
|
758
|
+
lastSeenAt: Date.now()
|
|
759
|
+
}))
|
|
760
|
+
return
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const turnId = notificationTurnId(notification)
|
|
764
|
+
if (panel.turnId && turnId && turnId !== panel.turnId && notification.method !== 'turn/started') {
|
|
765
|
+
return
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
switch (notification.method) {
|
|
769
|
+
case 'turn/started': {
|
|
770
|
+
upsertSubagentPanel(threadId, (existingPanel) => ({
|
|
771
|
+
...(existingPanel ?? createSubagentPanelState(threadId)),
|
|
772
|
+
turnId: notificationTurnId(notification),
|
|
773
|
+
status: existingPanel?.status === 'completed' ? existingPanel.status : 'running',
|
|
774
|
+
lastSeenAt: Date.now()
|
|
775
|
+
}))
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
case 'item/started': {
|
|
779
|
+
const params = notification.params as { item: CodexThreadItem }
|
|
780
|
+
if (params.item.type === 'collabAgentToolCall') {
|
|
781
|
+
applySubagentActivityItem(params.item)
|
|
782
|
+
}
|
|
783
|
+
seedSubagentStreamingMessage(threadId, params.item)
|
|
784
|
+
return
|
|
785
|
+
}
|
|
786
|
+
case 'item/completed': {
|
|
787
|
+
const params = notification.params as { item: CodexThreadItem }
|
|
788
|
+
if (params.item.type === 'collabAgentToolCall') {
|
|
789
|
+
applySubagentActivityItem(params.item)
|
|
790
|
+
}
|
|
791
|
+
for (const nextMessage of itemToMessages(params.item)) {
|
|
792
|
+
updateSubagentPanelMessages(threadId, (panelMessages) =>
|
|
793
|
+
upsertStreamingMessage(panelMessages, {
|
|
794
|
+
...nextMessage,
|
|
795
|
+
pending: false
|
|
796
|
+
})
|
|
797
|
+
)
|
|
798
|
+
}
|
|
799
|
+
return
|
|
800
|
+
}
|
|
801
|
+
case 'item/agentMessage/delta':
|
|
802
|
+
case 'item/plan/delta': {
|
|
803
|
+
const params = notification.params as { itemId: string, delta: string }
|
|
804
|
+
appendSubagentTextPartDelta(threadId, params.itemId, params.delta, {
|
|
805
|
+
id: params.itemId,
|
|
806
|
+
role: 'assistant',
|
|
807
|
+
pending: true,
|
|
808
|
+
parts: [{
|
|
809
|
+
type: 'text',
|
|
810
|
+
text: '',
|
|
811
|
+
state: 'streaming'
|
|
812
|
+
}]
|
|
813
|
+
})
|
|
814
|
+
return
|
|
815
|
+
}
|
|
816
|
+
case 'item/reasoning/textDelta':
|
|
817
|
+
case 'item/reasoning/summaryTextDelta': {
|
|
818
|
+
const params = notification.params as { itemId: string, delta: string }
|
|
819
|
+
updateSubagentMessage(threadId, params.itemId, {
|
|
820
|
+
id: params.itemId,
|
|
821
|
+
role: 'assistant',
|
|
822
|
+
pending: true,
|
|
823
|
+
parts: [{
|
|
824
|
+
type: 'reasoning',
|
|
825
|
+
summary: [],
|
|
826
|
+
content: [],
|
|
827
|
+
state: 'streaming'
|
|
828
|
+
}]
|
|
829
|
+
}, (message) => {
|
|
830
|
+
const partIndex = message.parts.findIndex(part => part.type === 'reasoning')
|
|
831
|
+
const existingPart = partIndex === -1
|
|
832
|
+
? {
|
|
833
|
+
type: 'reasoning' as const,
|
|
834
|
+
summary: [],
|
|
835
|
+
content: []
|
|
836
|
+
}
|
|
837
|
+
: message.parts[partIndex] as Extract<ChatPart, { type: 'reasoning' }>
|
|
838
|
+
const nextPart: Extract<ChatPart, { type: 'reasoning' }> = {
|
|
839
|
+
type: 'reasoning',
|
|
840
|
+
summary: notification.method === 'item/reasoning/summaryTextDelta'
|
|
841
|
+
? [...existingPart.summary, params.delta]
|
|
842
|
+
: existingPart.summary,
|
|
843
|
+
content: notification.method === 'item/reasoning/textDelta'
|
|
844
|
+
? [...existingPart.content, params.delta]
|
|
845
|
+
: existingPart.content,
|
|
846
|
+
state: 'streaming'
|
|
847
|
+
}
|
|
848
|
+
const nextParts = partIndex === -1
|
|
849
|
+
? [...message.parts, nextPart]
|
|
850
|
+
: message.parts.map((part, index) => index === partIndex ? nextPart : part)
|
|
851
|
+
|
|
852
|
+
return {
|
|
853
|
+
...message,
|
|
854
|
+
pending: true,
|
|
855
|
+
parts: nextParts
|
|
856
|
+
}
|
|
857
|
+
})
|
|
858
|
+
return
|
|
859
|
+
}
|
|
860
|
+
case 'item/commandExecution/outputDelta': {
|
|
861
|
+
const params = notification.params as { itemId: string, delta: string }
|
|
862
|
+
const fallbackItem = fallbackCommandItemData(params.itemId)
|
|
863
|
+
updateSubagentItemPart(threadId, params.itemId, fallbackCommandMessage(params.itemId), (itemData) => ({
|
|
864
|
+
kind: 'command_execution',
|
|
865
|
+
item: {
|
|
866
|
+
...(itemData.kind === 'command_execution' ? itemData.item : fallbackItem.item),
|
|
867
|
+
aggregatedOutput: `${(itemData.kind === 'command_execution' ? itemData.item.aggregatedOutput : '') ?? ''}${params.delta}`,
|
|
868
|
+
status: 'inProgress'
|
|
869
|
+
}
|
|
870
|
+
}))
|
|
871
|
+
return
|
|
872
|
+
}
|
|
873
|
+
case 'item/fileChange/outputDelta': {
|
|
874
|
+
const params = notification.params as { itemId: string, delta: string }
|
|
875
|
+
const fallbackItem = fallbackFileChangeItemData(params.itemId)
|
|
876
|
+
updateSubagentItemPart(threadId, params.itemId, fallbackFileChangeMessage(params.itemId), (itemData) => {
|
|
877
|
+
const baseItem: FileChangeItem = itemData.kind === 'file_change'
|
|
878
|
+
? itemData.item
|
|
879
|
+
: fallbackItem.item
|
|
880
|
+
return {
|
|
881
|
+
kind: 'file_change',
|
|
882
|
+
item: {
|
|
883
|
+
...baseItem,
|
|
884
|
+
liveOutput: `${baseItem.liveOutput ?? ''}${params.delta}`,
|
|
885
|
+
status: 'inProgress'
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
})
|
|
889
|
+
return
|
|
890
|
+
}
|
|
891
|
+
case 'item/mcpToolCall/progress': {
|
|
892
|
+
const params = notification.params as { itemId: string, message: string }
|
|
893
|
+
const fallbackItem = fallbackMcpToolItemData(params.itemId)
|
|
894
|
+
updateSubagentItemPart(threadId, params.itemId, fallbackMcpToolMessage(params.itemId), (itemData) => {
|
|
895
|
+
const baseItem: McpToolCallItem = itemData.kind === 'mcp_tool_call'
|
|
896
|
+
? itemData.item
|
|
897
|
+
: fallbackItem.item
|
|
898
|
+
return {
|
|
899
|
+
kind: 'mcp_tool_call',
|
|
900
|
+
item: {
|
|
901
|
+
...baseItem,
|
|
902
|
+
progressMessages: [...(baseItem.progressMessages ?? []), params.message],
|
|
903
|
+
status: 'inProgress'
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
})
|
|
907
|
+
return
|
|
908
|
+
}
|
|
909
|
+
case 'turn/completed': {
|
|
910
|
+
upsertSubagentPanel(threadId, (existingPanel) => ({
|
|
911
|
+
...(existingPanel ?? createSubagentPanelState(threadId)),
|
|
912
|
+
turnId: null,
|
|
913
|
+
status: isSubagentActiveStatus(existingPanel?.status ?? null)
|
|
914
|
+
? 'completed'
|
|
915
|
+
: existingPanel?.status ?? null,
|
|
916
|
+
lastSeenAt: Date.now()
|
|
917
|
+
}))
|
|
918
|
+
return
|
|
919
|
+
}
|
|
920
|
+
case 'turn/failed': {
|
|
921
|
+
const params = notification.params as { error?: { message?: string } }
|
|
922
|
+
const messageText = params.error?.message ?? 'The turn failed.'
|
|
923
|
+
pushSubagentEventMessage(threadId, 'turn.failed', messageText)
|
|
924
|
+
upsertSubagentPanel(threadId, (existingPanel) => ({
|
|
925
|
+
...(existingPanel ?? createSubagentPanelState(threadId)),
|
|
926
|
+
status: 'errored',
|
|
927
|
+
turnId: null,
|
|
928
|
+
lastSeenAt: Date.now()
|
|
929
|
+
}))
|
|
930
|
+
return
|
|
931
|
+
}
|
|
932
|
+
case 'stream/error': {
|
|
933
|
+
const params = notification.params as { message?: string }
|
|
934
|
+
const messageText = params.message ?? 'The stream failed.'
|
|
935
|
+
pushSubagentEventMessage(threadId, 'stream.error', messageText)
|
|
936
|
+
upsertSubagentPanel(threadId, (existingPanel) => ({
|
|
937
|
+
...(existingPanel ?? createSubagentPanelState(threadId)),
|
|
938
|
+
status: 'errored',
|
|
939
|
+
turnId: null,
|
|
940
|
+
lastSeenAt: Date.now()
|
|
941
|
+
}))
|
|
942
|
+
return
|
|
943
|
+
}
|
|
944
|
+
default:
|
|
945
|
+
return
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const applyNotification = (notification: CodexRpcNotification) => {
|
|
950
|
+
switch (notification.method) {
|
|
951
|
+
case 'thread/started': {
|
|
952
|
+
const nextThreadId = notificationThreadId(notification)
|
|
953
|
+
if (!nextThreadId) {
|
|
954
|
+
return
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
activeThreadId.value = nextThreadId
|
|
958
|
+
pendingThreadId.value = pendingThreadId.value ?? nextThreadId
|
|
959
|
+
return
|
|
960
|
+
}
|
|
961
|
+
case 'turn/started': {
|
|
962
|
+
messages.value = upsertStreamingMessage(
|
|
963
|
+
messages.value,
|
|
964
|
+
eventToMessage(`event-turn-started-${notificationTurnId(notification) ?? Date.now()}`, {
|
|
965
|
+
kind: 'turn.started'
|
|
966
|
+
})
|
|
967
|
+
)
|
|
968
|
+
status.value = 'streaming'
|
|
969
|
+
return
|
|
970
|
+
}
|
|
971
|
+
case 'item/started': {
|
|
972
|
+
const params = notification.params as { item: CodexThreadItem }
|
|
973
|
+
if (params.item.type === 'collabAgentToolCall') {
|
|
974
|
+
applySubagentActivityItem(params.item)
|
|
975
|
+
}
|
|
976
|
+
seedStreamingMessage(params.item)
|
|
977
|
+
status.value = 'streaming'
|
|
978
|
+
return
|
|
979
|
+
}
|
|
980
|
+
case 'item/completed': {
|
|
981
|
+
const params = notification.params as { item: CodexThreadItem }
|
|
982
|
+
if (params.item.type === 'collabAgentToolCall') {
|
|
983
|
+
applySubagentActivityItem(params.item)
|
|
984
|
+
}
|
|
985
|
+
if (params.item.type === 'userMessage') {
|
|
986
|
+
stripOptimisticDraftMessages()
|
|
987
|
+
}
|
|
988
|
+
for (const nextMessage of itemToMessages(params.item)) {
|
|
989
|
+
messages.value = upsertStreamingMessage(messages.value, {
|
|
990
|
+
...nextMessage,
|
|
991
|
+
pending: false
|
|
992
|
+
})
|
|
993
|
+
}
|
|
994
|
+
return
|
|
995
|
+
}
|
|
996
|
+
case 'item/agentMessage/delta': {
|
|
997
|
+
const params = notification.params as { itemId: string, delta: string }
|
|
998
|
+
appendTextPartDelta(params.itemId, params.delta, {
|
|
999
|
+
id: params.itemId,
|
|
1000
|
+
role: 'assistant',
|
|
1001
|
+
pending: true,
|
|
1002
|
+
parts: [{
|
|
1003
|
+
type: 'text',
|
|
1004
|
+
text: '',
|
|
1005
|
+
state: 'streaming'
|
|
1006
|
+
}]
|
|
1007
|
+
})
|
|
1008
|
+
status.value = 'streaming'
|
|
1009
|
+
return
|
|
1010
|
+
}
|
|
1011
|
+
case 'item/plan/delta': {
|
|
1012
|
+
const params = notification.params as { itemId: string, delta: string }
|
|
1013
|
+
appendTextPartDelta(params.itemId, params.delta, {
|
|
1014
|
+
id: params.itemId,
|
|
1015
|
+
role: 'assistant',
|
|
1016
|
+
pending: true,
|
|
1017
|
+
parts: [{
|
|
1018
|
+
type: 'text',
|
|
1019
|
+
text: '',
|
|
1020
|
+
state: 'streaming'
|
|
1021
|
+
}]
|
|
1022
|
+
})
|
|
1023
|
+
status.value = 'streaming'
|
|
1024
|
+
return
|
|
1025
|
+
}
|
|
1026
|
+
case 'item/reasoning/textDelta':
|
|
1027
|
+
case 'item/reasoning/summaryTextDelta': {
|
|
1028
|
+
const params = notification.params as { itemId: string, delta: string }
|
|
1029
|
+
updateMessage(params.itemId, {
|
|
1030
|
+
id: params.itemId,
|
|
1031
|
+
role: 'assistant',
|
|
1032
|
+
pending: true,
|
|
1033
|
+
parts: [{
|
|
1034
|
+
type: 'reasoning',
|
|
1035
|
+
summary: [],
|
|
1036
|
+
content: [],
|
|
1037
|
+
state: 'streaming'
|
|
1038
|
+
}]
|
|
1039
|
+
}, (message) => {
|
|
1040
|
+
const partIndex = message.parts.findIndex(part => part.type === 'reasoning')
|
|
1041
|
+
const existingPart = partIndex === -1
|
|
1042
|
+
? {
|
|
1043
|
+
type: 'reasoning' as const,
|
|
1044
|
+
summary: [],
|
|
1045
|
+
content: []
|
|
1046
|
+
}
|
|
1047
|
+
: message.parts[partIndex] as Extract<ChatPart, { type: 'reasoning' }>
|
|
1048
|
+
const nextPart: Extract<ChatPart, { type: 'reasoning' }> = {
|
|
1049
|
+
type: 'reasoning',
|
|
1050
|
+
summary: notification.method === 'item/reasoning/summaryTextDelta'
|
|
1051
|
+
? [...existingPart.summary, params.delta]
|
|
1052
|
+
: existingPart.summary,
|
|
1053
|
+
content: notification.method === 'item/reasoning/textDelta'
|
|
1054
|
+
? [...existingPart.content, params.delta]
|
|
1055
|
+
: existingPart.content,
|
|
1056
|
+
state: 'streaming'
|
|
1057
|
+
}
|
|
1058
|
+
const nextParts = partIndex === -1
|
|
1059
|
+
? [...message.parts, nextPart]
|
|
1060
|
+
: message.parts.map((part, index) => index === partIndex ? nextPart : part)
|
|
1061
|
+
|
|
1062
|
+
return {
|
|
1063
|
+
...message,
|
|
1064
|
+
pending: true,
|
|
1065
|
+
parts: nextParts
|
|
1066
|
+
}
|
|
1067
|
+
})
|
|
1068
|
+
status.value = 'streaming'
|
|
1069
|
+
return
|
|
1070
|
+
}
|
|
1071
|
+
case 'item/commandExecution/outputDelta': {
|
|
1072
|
+
const params = notification.params as { itemId: string, delta: string }
|
|
1073
|
+
const fallbackItem = fallbackCommandItemData(params.itemId)
|
|
1074
|
+
updateItemPart(params.itemId, fallbackCommandMessage(params.itemId), (itemData) => ({
|
|
1075
|
+
kind: 'command_execution',
|
|
1076
|
+
item: {
|
|
1077
|
+
...(itemData.kind === 'command_execution' ? itemData.item : fallbackItem.item),
|
|
1078
|
+
aggregatedOutput: `${(itemData.kind === 'command_execution' ? itemData.item.aggregatedOutput : '') ?? ''}${params.delta}`,
|
|
1079
|
+
status: 'inProgress'
|
|
1080
|
+
}
|
|
1081
|
+
}))
|
|
1082
|
+
status.value = 'streaming'
|
|
1083
|
+
return
|
|
1084
|
+
}
|
|
1085
|
+
case 'item/fileChange/outputDelta': {
|
|
1086
|
+
const params = notification.params as { itemId: string, delta: string }
|
|
1087
|
+
const fallbackItem = fallbackFileChangeItemData(params.itemId)
|
|
1088
|
+
updateItemPart(params.itemId, fallbackFileChangeMessage(params.itemId), (itemData) => {
|
|
1089
|
+
const baseItem: FileChangeItem = itemData.kind === 'file_change'
|
|
1090
|
+
? itemData.item
|
|
1091
|
+
: fallbackItem.item
|
|
1092
|
+
return {
|
|
1093
|
+
kind: 'file_change',
|
|
1094
|
+
item: {
|
|
1095
|
+
...baseItem,
|
|
1096
|
+
liveOutput: `${baseItem.liveOutput ?? ''}${params.delta}`,
|
|
1097
|
+
status: 'inProgress'
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
})
|
|
1101
|
+
status.value = 'streaming'
|
|
1102
|
+
return
|
|
1103
|
+
}
|
|
1104
|
+
case 'item/mcpToolCall/progress': {
|
|
1105
|
+
const params = notification.params as { itemId: string, message: string }
|
|
1106
|
+
const fallbackItem = fallbackMcpToolItemData(params.itemId)
|
|
1107
|
+
updateItemPart(params.itemId, fallbackMcpToolMessage(params.itemId), (itemData) => {
|
|
1108
|
+
const baseItem: McpToolCallItem = itemData.kind === 'mcp_tool_call'
|
|
1109
|
+
? itemData.item
|
|
1110
|
+
: fallbackItem.item
|
|
1111
|
+
return {
|
|
1112
|
+
kind: 'mcp_tool_call',
|
|
1113
|
+
item: {
|
|
1114
|
+
...baseItem,
|
|
1115
|
+
progressMessages: [...(baseItem.progressMessages ?? []), params.message],
|
|
1116
|
+
status: 'inProgress'
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
})
|
|
1120
|
+
status.value = 'streaming'
|
|
1121
|
+
return
|
|
1122
|
+
}
|
|
1123
|
+
case 'turn/failed': {
|
|
1124
|
+
const params = notification.params as { error?: { message?: string } }
|
|
1125
|
+
const messageText = params.error?.message ?? 'The turn failed.'
|
|
1126
|
+
pushEventMessage('turn.failed', messageText)
|
|
1127
|
+
clearLiveStream()
|
|
1128
|
+
error.value = messageText
|
|
1129
|
+
status.value = 'error'
|
|
1130
|
+
return
|
|
1131
|
+
}
|
|
1132
|
+
case 'stream/error': {
|
|
1133
|
+
const params = notification.params as { message?: string }
|
|
1134
|
+
const messageText = params.message ?? 'The stream failed.'
|
|
1135
|
+
pushEventMessage('stream.error', messageText)
|
|
1136
|
+
clearLiveStream()
|
|
1137
|
+
error.value = messageText
|
|
1138
|
+
status.value = 'error'
|
|
1139
|
+
return
|
|
1140
|
+
}
|
|
1141
|
+
case 'turn/completed': {
|
|
1142
|
+
status.value = 'ready'
|
|
1143
|
+
clearLiveStream()
|
|
1144
|
+
return
|
|
1145
|
+
}
|
|
1146
|
+
default:
|
|
1147
|
+
return
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const sendMessage = async () => {
|
|
1152
|
+
const text = input.value.trim()
|
|
1153
|
+
if (!text) {
|
|
1154
|
+
return
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
pinnedToBottom.value = true
|
|
1158
|
+
error.value = null
|
|
1159
|
+
status.value = 'submitted'
|
|
1160
|
+
input.value = ''
|
|
1161
|
+
const shouldRenderOptimisticDraft = !routeThreadId.value
|
|
1162
|
+
|
|
1163
|
+
if (shouldRenderOptimisticDraft) {
|
|
1164
|
+
const optimisticMessage: ChatMessage = {
|
|
1165
|
+
id: `local-user-${Date.now()}`,
|
|
1166
|
+
role: 'user',
|
|
1167
|
+
parts: [{
|
|
1168
|
+
type: 'text',
|
|
1169
|
+
text,
|
|
1170
|
+
state: 'done'
|
|
1171
|
+
}]
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
messages.value = [...messages.value, optimisticMessage]
|
|
1175
|
+
}
|
|
1176
|
+
let unsubscribe: (() => void) | null = null
|
|
1177
|
+
|
|
1178
|
+
try {
|
|
1179
|
+
await ensureProjectRuntime()
|
|
1180
|
+
const { threadId, created } = await ensureThread()
|
|
1181
|
+
const client = getClient(props.projectId)
|
|
1182
|
+
const buffered: CodexRpcNotification[] = []
|
|
1183
|
+
let currentTurnId: string | null = null
|
|
1184
|
+
let completionResolved = false
|
|
1185
|
+
let resolveCompletion: (() => void) | null = null
|
|
1186
|
+
let rejectCompletion: ((error: Error) => void) | null = null
|
|
1187
|
+
|
|
1188
|
+
const settleNotification = (notification: CodexRpcNotification) => {
|
|
1189
|
+
if (notificationThreadId(notification) !== threadId) {
|
|
1190
|
+
return false
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const turnId = notificationTurnId(notification)
|
|
1194
|
+
if (currentTurnId && turnId && turnId !== currentTurnId) {
|
|
1195
|
+
return false
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (notification.method === 'error') {
|
|
1199
|
+
completionResolved = true
|
|
1200
|
+
unsubscribe?.()
|
|
1201
|
+
if (session.liveStream?.unsubscribe === unsubscribe) {
|
|
1202
|
+
session.liveStream = null
|
|
1203
|
+
}
|
|
1204
|
+
const params = notification.params as { error?: { message?: string } }
|
|
1205
|
+
const messageText = params.error?.message ?? 'Turn failed.'
|
|
1206
|
+
pushEventMessage('stream.error', messageText)
|
|
1207
|
+
rejectCompletion?.(new Error(messageText))
|
|
1208
|
+
return true
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
applyNotification(notification)
|
|
1212
|
+
|
|
1213
|
+
if (notification.method === 'turn/completed') {
|
|
1214
|
+
completionResolved = true
|
|
1215
|
+
unsubscribe?.()
|
|
1216
|
+
if (session.liveStream?.unsubscribe === unsubscribe) {
|
|
1217
|
+
session.liveStream = null
|
|
1218
|
+
}
|
|
1219
|
+
resolveCompletion?.()
|
|
1220
|
+
return true
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (notification.method === 'turn/failed' || notification.method === 'stream/error') {
|
|
1224
|
+
completionResolved = true
|
|
1225
|
+
unsubscribe?.()
|
|
1226
|
+
if (session.liveStream?.unsubscribe === unsubscribe) {
|
|
1227
|
+
session.liveStream = null
|
|
1228
|
+
}
|
|
1229
|
+
rejectCompletion?.(new Error(error.value ?? 'Turn failed.'))
|
|
1230
|
+
return true
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return false
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
clearLiveStream()
|
|
1237
|
+
|
|
1238
|
+
const completion = new Promise<void>((resolve, reject) => {
|
|
1239
|
+
resolveCompletion = resolve
|
|
1240
|
+
rejectCompletion = (caughtError: Error) => reject(caughtError)
|
|
1241
|
+
})
|
|
1242
|
+
|
|
1243
|
+
const liveStream = {
|
|
1244
|
+
threadId,
|
|
1245
|
+
turnId: null as string | null,
|
|
1246
|
+
bufferedNotifications: buffered,
|
|
1247
|
+
observedSubagentThreadIds: new Set<string>(),
|
|
1248
|
+
unsubscribe: null as (() => void) | null
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
unsubscribe = client.subscribe((notification) => {
|
|
1252
|
+
const targetThreadId = notificationThreadId(notification)
|
|
1253
|
+
if (!targetThreadId) {
|
|
1254
|
+
return
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (targetThreadId !== threadId) {
|
|
1258
|
+
if (liveStream.observedSubagentThreadIds.has(targetThreadId)) {
|
|
1259
|
+
applySubagentNotification(targetThreadId, notification)
|
|
1260
|
+
}
|
|
1261
|
+
return
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
if (!currentTurnId) {
|
|
1265
|
+
buffered.push(notification)
|
|
1266
|
+
return
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
settleNotification(notification)
|
|
1270
|
+
})
|
|
1271
|
+
liveStream.unsubscribe = unsubscribe
|
|
1272
|
+
session.liveStream = liveStream
|
|
1273
|
+
|
|
1274
|
+
if (created && !routeThreadId.value) {
|
|
1275
|
+
pendingThreadId.value = threadId
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const turnStart = await client.request<TurnStartResponse>('turn/start', {
|
|
1279
|
+
threadId,
|
|
1280
|
+
input: [{
|
|
1281
|
+
type: 'text',
|
|
1282
|
+
text,
|
|
1283
|
+
text_elements: []
|
|
1284
|
+
}],
|
|
1285
|
+
cwd: selectedProject.value?.projectPath ?? null,
|
|
1286
|
+
approvalPolicy: 'never'
|
|
1287
|
+
})
|
|
1288
|
+
|
|
1289
|
+
currentTurnId = turnStart.turn.id
|
|
1290
|
+
liveStream.turnId = turnStart.turn.id
|
|
1291
|
+
|
|
1292
|
+
for (const notification of buffered.splice(0, buffered.length)) {
|
|
1293
|
+
if (settleNotification(notification) && completionResolved) {
|
|
1294
|
+
break
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
await completion
|
|
1299
|
+
status.value = 'ready'
|
|
1300
|
+
|
|
1301
|
+
if (routeThreadId.value) {
|
|
1302
|
+
await hydrateThread(threadId)
|
|
1303
|
+
return
|
|
1304
|
+
}
|
|
1305
|
+
} catch (caughtError) {
|
|
1306
|
+
unsubscribe?.()
|
|
1307
|
+
if (session.liveStream?.unsubscribe === unsubscribe) {
|
|
1308
|
+
session.liveStream = null
|
|
1309
|
+
}
|
|
1310
|
+
error.value = caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
1311
|
+
status.value = 'error'
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
const sendStarterPrompt = async (text: string) => {
|
|
1316
|
+
if (isBusy.value) {
|
|
1317
|
+
return
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
input.value = text
|
|
1321
|
+
await sendMessage()
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const onPromptEnter = (event: KeyboardEvent) => {
|
|
1325
|
+
if (!shouldSubmit(event)) {
|
|
1326
|
+
return
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
event.preventDefault()
|
|
1330
|
+
void sendMessage()
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
onMounted(() => {
|
|
1334
|
+
if (!loaded.value) {
|
|
1335
|
+
void refreshProjects()
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
void scheduleScrollToBottom('auto')
|
|
1339
|
+
})
|
|
1340
|
+
|
|
1341
|
+
watch(() => props.threadId ?? null, (threadId) => {
|
|
1342
|
+
if (!threadId) {
|
|
1343
|
+
if (isBusy.value || pendingThreadId.value) {
|
|
1344
|
+
return
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
resetDraftThread()
|
|
1348
|
+
return
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
if (
|
|
1352
|
+
autoRedirectThreadId.value === threadId
|
|
1353
|
+
&& activeThreadId.value === threadId
|
|
1354
|
+
&& isBusy.value
|
|
1355
|
+
) {
|
|
1356
|
+
autoRedirectThreadId.value = null
|
|
1357
|
+
pendingThreadId.value = null
|
|
1358
|
+
return
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
void hydrateThread(threadId)
|
|
1362
|
+
}, { immediate: true })
|
|
1363
|
+
|
|
1364
|
+
watch(pendingThreadId, async (threadId) => {
|
|
1365
|
+
if (!threadId) {
|
|
1366
|
+
return
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
if (routeThreadId.value === threadId) {
|
|
1370
|
+
pendingThreadId.value = null
|
|
1371
|
+
autoRedirectThreadId.value = null
|
|
1372
|
+
return
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
autoRedirectThreadId.value = threadId
|
|
1376
|
+
await router.push(toProjectThreadRoute(props.projectId, threadId))
|
|
1377
|
+
pendingThreadId.value = null
|
|
1378
|
+
})
|
|
1379
|
+
|
|
1380
|
+
watch(messages, () => {
|
|
1381
|
+
void scheduleScrollToBottom(status.value === 'streaming' ? 'auto' : 'smooth')
|
|
1382
|
+
}, { flush: 'post' })
|
|
1383
|
+
|
|
1384
|
+
watch(status, (nextStatus, previousStatus) => {
|
|
1385
|
+
if (nextStatus === previousStatus) {
|
|
1386
|
+
return
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
void scheduleScrollToBottom(nextStatus === 'streaming' ? 'auto' : 'smooth')
|
|
1390
|
+
}, { flush: 'post' })
|
|
1391
|
+
</script>
|
|
1392
|
+
|
|
1393
|
+
<template>
|
|
1394
|
+
<section class="flex h-full min-h-0 flex-col bg-default">
|
|
1395
|
+
<div
|
|
1396
|
+
ref="scrollViewport"
|
|
1397
|
+
class="min-h-0 flex-1 overflow-y-auto"
|
|
1398
|
+
@scroll="updatePinnedState"
|
|
1399
|
+
>
|
|
1400
|
+
<div
|
|
1401
|
+
v-if="showWelcomeState"
|
|
1402
|
+
class="flex min-h-full items-center justify-center px-6 py-10"
|
|
1403
|
+
>
|
|
1404
|
+
<div class="flex w-full max-w-4xl flex-col items-center gap-10 text-center">
|
|
1405
|
+
<div class="space-y-4">
|
|
1406
|
+
<div class="text-xs font-medium uppercase tracking-[0.28em] text-primary">
|
|
1407
|
+
Ready To Code
|
|
1408
|
+
</div>
|
|
1409
|
+
<div class="space-y-2">
|
|
1410
|
+
<h1 class="text-balance text-4xl font-semibold tracking-tight text-highlighted md:text-5xl">
|
|
1411
|
+
Let's build
|
|
1412
|
+
</h1>
|
|
1413
|
+
<p class="text-balance text-3xl font-medium tracking-tight text-toned md:text-4xl">
|
|
1414
|
+
{{ projectTitle }}
|
|
1415
|
+
</p>
|
|
1416
|
+
</div>
|
|
1417
|
+
<p class="mx-auto max-w-2xl text-base leading-7 text-muted md:text-lg">
|
|
1418
|
+
Start with a goal, a bug, or a question. Codori will start the runtime when needed and keep the thread ready to continue.
|
|
1419
|
+
</p>
|
|
1420
|
+
</div>
|
|
1421
|
+
|
|
1422
|
+
<div class="grid w-full gap-3 md:grid-cols-3">
|
|
1423
|
+
<button
|
|
1424
|
+
v-for="prompt in starterPrompts"
|
|
1425
|
+
:key="prompt.title"
|
|
1426
|
+
type="button"
|
|
1427
|
+
class="rounded-3xl border border-default/70 bg-elevated/25 px-5 py-5 text-left transition hover:border-primary/30 hover:bg-elevated/45"
|
|
1428
|
+
@click="sendStarterPrompt(prompt.text)"
|
|
1429
|
+
>
|
|
1430
|
+
<div class="text-sm font-semibold text-highlighted">
|
|
1431
|
+
{{ prompt.title }}
|
|
1432
|
+
</div>
|
|
1433
|
+
<p class="mt-3 text-sm leading-6 text-muted">
|
|
1434
|
+
{{ prompt.text }}
|
|
1435
|
+
</p>
|
|
1436
|
+
</button>
|
|
1437
|
+
</div>
|
|
1438
|
+
</div>
|
|
1439
|
+
</div>
|
|
1440
|
+
|
|
1441
|
+
<UChatMessages
|
|
1442
|
+
v-else
|
|
1443
|
+
:messages="messages"
|
|
1444
|
+
:status="status"
|
|
1445
|
+
:should-auto-scroll="false"
|
|
1446
|
+
:should-scroll-to-bottom="false"
|
|
1447
|
+
:auto-scroll="false"
|
|
1448
|
+
:spacing-offset="140"
|
|
1449
|
+
:user="{
|
|
1450
|
+
ui: {
|
|
1451
|
+
root: 'scroll-mt-4',
|
|
1452
|
+
container: 'gap-3 pb-8',
|
|
1453
|
+
content: 'px-4 py-3 rounded-2xl min-h-12'
|
|
1454
|
+
}
|
|
1455
|
+
}"
|
|
1456
|
+
:ui="{
|
|
1457
|
+
root: 'min-h-full px-4 py-5 md:px-6',
|
|
1458
|
+
message: 'max-w-none',
|
|
1459
|
+
content: 'w-full max-w-5xl'
|
|
1460
|
+
}"
|
|
1461
|
+
compact
|
|
1462
|
+
>
|
|
1463
|
+
<template #content="{ message }">
|
|
1464
|
+
<MessageContent :message="message as ChatMessage" />
|
|
1465
|
+
</template>
|
|
1466
|
+
</UChatMessages>
|
|
1467
|
+
</div>
|
|
1468
|
+
|
|
1469
|
+
<div class="sticky bottom-0 shrink-0 border-t border-default bg-default/95 px-4 py-3 backdrop-blur md:px-6">
|
|
1470
|
+
<div class="mx-auto w-full max-w-5xl">
|
|
1471
|
+
<TunnelNotice class="mb-3" />
|
|
1472
|
+
<UAlert
|
|
1473
|
+
v-if="error"
|
|
1474
|
+
color="error"
|
|
1475
|
+
variant="soft"
|
|
1476
|
+
icon="i-lucide-circle-alert"
|
|
1477
|
+
:title="error"
|
|
1478
|
+
class="mb-3"
|
|
1479
|
+
/>
|
|
1480
|
+
|
|
1481
|
+
<UChatPrompt
|
|
1482
|
+
v-model="input"
|
|
1483
|
+
placeholder="Describe the change you want Codex to make"
|
|
1484
|
+
:error="submitError"
|
|
1485
|
+
:disabled="isBusy"
|
|
1486
|
+
autoresize
|
|
1487
|
+
@submit.prevent="sendMessage"
|
|
1488
|
+
@keydown.enter="onPromptEnter"
|
|
1489
|
+
@compositionstart="onCompositionStart"
|
|
1490
|
+
@compositionend="onCompositionEnd"
|
|
1491
|
+
>
|
|
1492
|
+
<UChatPromptSubmit
|
|
1493
|
+
:status="status"
|
|
1494
|
+
/>
|
|
1495
|
+
</UChatPrompt>
|
|
1496
|
+
</div>
|
|
1497
|
+
</div>
|
|
1498
|
+
</section>
|
|
1499
|
+
</template>
|