@codori/client 0.0.1 → 0.0.3
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/app/components/ChatWorkspace.vue +882 -187
- package/app/components/MessageContent.vue +4 -2
- package/app/components/MessagePartRenderer.ts +12 -2
- package/app/components/ProjectSidebar.vue +2 -2
- package/app/components/SubagentDrawerList.vue +64 -0
- package/app/components/SubagentTranscriptPanel.vue +305 -0
- package/app/components/ThreadList.vue +5 -5
- package/app/components/ThreadPanel.vue +65 -46
- package/app/components/VisualSubagentStack.vue +14 -244
- package/app/components/message-item/CommandExecution.vue +2 -2
- package/app/components/message-item/DynamicToolCall.vue +2 -2
- package/app/components/message-item/FileChange.vue +2 -2
- package/app/components/message-item/McpToolCall.vue +2 -2
- package/app/components/message-item/SubagentActivity.vue +2 -2
- package/app/components/message-item/WebSearch.vue +2 -2
- package/app/components/message-part/Attachment.vue +61 -0
- package/app/components/message-part/Event.vue +1 -1
- package/app/components/message-part/Item.ts +1 -1
- package/app/composables/useChatAttachments.ts +208 -0
- package/app/composables/useChatSession.ts +33 -2
- package/app/composables/useProjects.ts +4 -5
- package/app/composables/useRpc.ts +3 -3
- package/app/composables/useVisualSubagentPanels.ts +1 -1
- package/app/pages/projects/[...projectId]/index.vue +5 -5
- package/app/pages/projects/[...projectId]/threads/[threadId].vue +228 -75
- package/app/utils/chat-turn-engagement.ts +46 -0
- package/package.json +1 -1
- package/server/api/codori/projects/[projectId]/attachments/file.get.ts +62 -0
- package/server/api/codori/projects/[projectId]/attachments.post.ts +53 -0
- package/server/api/codori/projects/[projectId]/start.post.ts +3 -3
- package/server/api/codori/projects/[projectId]/status.get.ts +3 -3
- package/server/api/codori/projects/[projectId]/stop.post.ts +3 -3
- package/server/api/codori/projects/[projectId].get.ts +3 -3
- package/server/api/codori/projects/index.get.ts +2 -2
- package/server/utils/server-proxy.ts +23 -0
- package/shared/chat-attachments.ts +135 -0
- package/shared/chat-prompt-controls.ts +339 -0
- package/shared/codex-chat.ts +33 -11
- package/shared/codex-rpc.ts +19 -0
- package/shared/network.ts +8 -0
- package/shared/subagent-panels.ts +158 -0
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
import { useRouter } from '#imports'
|
|
3
3
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
4
4
|
import MessageContent from './MessageContent.vue'
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import {
|
|
6
|
+
reconcileOptimisticUserMessage,
|
|
7
|
+
removeChatMessage,
|
|
8
|
+
removePendingUserMessageId,
|
|
9
|
+
resolvePromptSubmitStatus,
|
|
10
|
+
resolveTurnSubmissionMethod,
|
|
11
|
+
shouldIgnoreNotificationAfterInterrupt
|
|
12
|
+
} from '../utils/chat-turn-engagement'
|
|
13
|
+
import { useChatAttachments, type DraftAttachment } from '../composables/useChatAttachments'
|
|
14
|
+
import { useChatSession, type LiveStream, type SubagentPanelState } from '../composables/useChatSession'
|
|
15
|
+
import { useProjects } from '../composables/useProjects'
|
|
16
|
+
import { useRpc } from '../composables/useRpc'
|
|
17
|
+
import { useChatSubmitGuard } from '../composables/useChatSubmitGuard'
|
|
9
18
|
import {
|
|
10
19
|
ITEM_PART,
|
|
11
20
|
eventToMessage,
|
|
@@ -18,8 +27,11 @@ import {
|
|
|
18
27
|
type FileChangeItem,
|
|
19
28
|
type ItemData,
|
|
20
29
|
type McpToolCallItem
|
|
21
|
-
} from '~~/shared/codex-chat
|
|
30
|
+
} from '~~/shared/codex-chat'
|
|
31
|
+
import { buildTurnStartInput } from '~~/shared/chat-attachments'
|
|
22
32
|
import {
|
|
33
|
+
type ConfigReadResponse,
|
|
34
|
+
type ModelListResponse,
|
|
23
35
|
notificationThreadId,
|
|
24
36
|
notificationTurnId,
|
|
25
37
|
type CodexRpcNotification,
|
|
@@ -29,8 +41,23 @@ import {
|
|
|
29
41
|
type ThreadResumeResponse,
|
|
30
42
|
type ThreadStartResponse,
|
|
31
43
|
type TurnStartResponse
|
|
32
|
-
} from '~~/shared/codex-rpc
|
|
33
|
-
import {
|
|
44
|
+
} from '~~/shared/codex-rpc'
|
|
45
|
+
import {
|
|
46
|
+
buildTurnOverrides,
|
|
47
|
+
coercePromptSelection,
|
|
48
|
+
ensureModelOption,
|
|
49
|
+
FALLBACK_MODELS,
|
|
50
|
+
formatCompactTokenCount,
|
|
51
|
+
formatReasoningEffortLabel,
|
|
52
|
+
normalizeConfigDefaults,
|
|
53
|
+
normalizeModelList,
|
|
54
|
+
normalizeThreadTokenUsage,
|
|
55
|
+
resolveContextWindowState,
|
|
56
|
+
resolveEffortOptions,
|
|
57
|
+
visibleModelOptions,
|
|
58
|
+
type ReasoningEffort
|
|
59
|
+
} from '~~/shared/chat-prompt-controls'
|
|
60
|
+
import { toProjectThreadRoute } from '~~/shared/codori'
|
|
34
61
|
|
|
35
62
|
const props = defineProps<{
|
|
36
63
|
projectId: string
|
|
@@ -50,6 +77,25 @@ const {
|
|
|
50
77
|
onCompositionEnd,
|
|
51
78
|
shouldSubmit
|
|
52
79
|
} = useChatSubmitGuard()
|
|
80
|
+
const {
|
|
81
|
+
attachments,
|
|
82
|
+
isDragging,
|
|
83
|
+
isUploading,
|
|
84
|
+
error: attachmentError,
|
|
85
|
+
fileInput,
|
|
86
|
+
removeAttachment,
|
|
87
|
+
replaceAttachments,
|
|
88
|
+
clearAttachments,
|
|
89
|
+
discardSnapshot,
|
|
90
|
+
openFilePicker,
|
|
91
|
+
onFileInputChange,
|
|
92
|
+
onPaste,
|
|
93
|
+
onDragEnter,
|
|
94
|
+
onDragLeave,
|
|
95
|
+
onDragOver,
|
|
96
|
+
onDrop,
|
|
97
|
+
uploadAttachments
|
|
98
|
+
} = useChatAttachments(props.projectId)
|
|
53
99
|
|
|
54
100
|
const input = ref('')
|
|
55
101
|
const scrollViewport = ref<HTMLElement | null>(null)
|
|
@@ -64,12 +110,39 @@ const {
|
|
|
64
110
|
threadTitle,
|
|
65
111
|
pendingThreadId,
|
|
66
112
|
autoRedirectThreadId,
|
|
67
|
-
loadVersion
|
|
113
|
+
loadVersion,
|
|
114
|
+
promptControlsLoaded,
|
|
115
|
+
promptControlsLoading,
|
|
116
|
+
availableModels,
|
|
117
|
+
selectedModel,
|
|
118
|
+
selectedEffort,
|
|
119
|
+
modelContextWindow,
|
|
120
|
+
tokenUsage
|
|
68
121
|
} = session
|
|
69
122
|
|
|
70
123
|
const selectedProject = computed(() => getProject(props.projectId))
|
|
71
|
-
const
|
|
72
|
-
const
|
|
124
|
+
const composerError = computed(() => attachmentError.value ?? error.value)
|
|
125
|
+
const submitError = computed(() => composerError.value ? new Error(composerError.value) : undefined)
|
|
126
|
+
const interruptRequested = ref(false)
|
|
127
|
+
const isBusy = computed(() =>
|
|
128
|
+
status.value === 'submitted'
|
|
129
|
+
|| status.value === 'streaming'
|
|
130
|
+
|| isUploading.value
|
|
131
|
+
)
|
|
132
|
+
const hasDraftContent = computed(() =>
|
|
133
|
+
input.value.trim().length > 0
|
|
134
|
+
|| attachments.value.length > 0
|
|
135
|
+
)
|
|
136
|
+
const isComposerDisabled = computed(() =>
|
|
137
|
+
isUploading.value
|
|
138
|
+
|| interruptRequested.value
|
|
139
|
+
)
|
|
140
|
+
const promptSubmitStatus = computed(() =>
|
|
141
|
+
resolvePromptSubmitStatus({
|
|
142
|
+
status: status.value,
|
|
143
|
+
hasDraftContent: hasDraftContent.value
|
|
144
|
+
})
|
|
145
|
+
)
|
|
73
146
|
const routeThreadId = computed(() => props.threadId ?? null)
|
|
74
147
|
const projectTitle = computed(() => selectedProject.value?.projectId ?? props.projectId)
|
|
75
148
|
const showWelcomeState = computed(() =>
|
|
@@ -98,7 +171,134 @@ const starterPrompts = computed(() => {
|
|
|
98
171
|
]
|
|
99
172
|
})
|
|
100
173
|
|
|
174
|
+
const hasKnownThreadUsage = computed(() => !activeThreadId.value || tokenUsage.value !== null)
|
|
175
|
+
const effectiveModelList = computed(() => {
|
|
176
|
+
const withSelected = ensureModelOption(
|
|
177
|
+
availableModels.value.length > 0 ? availableModels.value : FALLBACK_MODELS,
|
|
178
|
+
selectedModel.value,
|
|
179
|
+
selectedEffort.value
|
|
180
|
+
)
|
|
181
|
+
return visibleModelOptions(withSelected)
|
|
182
|
+
})
|
|
183
|
+
const selectedModelOption = computed(() =>
|
|
184
|
+
effectiveModelList.value.find(model => model.model === selectedModel.value)
|
|
185
|
+
?? effectiveModelList.value[0]
|
|
186
|
+
?? FALLBACK_MODELS[0]
|
|
187
|
+
)
|
|
188
|
+
const modelSelectItems = computed(() =>
|
|
189
|
+
effectiveModelList.value.map(model => ({
|
|
190
|
+
label: model.displayName,
|
|
191
|
+
value: model.model
|
|
192
|
+
}))
|
|
193
|
+
)
|
|
194
|
+
const effortOptions = computed(() => resolveEffortOptions(effectiveModelList.value, selectedModel.value))
|
|
195
|
+
const effortSelectItems = computed(() =>
|
|
196
|
+
effortOptions.value.map(effort => ({
|
|
197
|
+
label: formatReasoningEffortLabel(effort),
|
|
198
|
+
value: effort
|
|
199
|
+
}))
|
|
200
|
+
)
|
|
201
|
+
const contextWindowState = computed(() =>
|
|
202
|
+
resolveContextWindowState(tokenUsage.value, modelContextWindow.value, hasKnownThreadUsage.value)
|
|
203
|
+
)
|
|
204
|
+
const contextUsedPercent = computed(() => contextWindowState.value.usedPercent ?? 0)
|
|
205
|
+
const contextIndicatorLabel = computed(() => {
|
|
206
|
+
const remainingPercent = contextWindowState.value.remainingPercent
|
|
207
|
+
return remainingPercent == null ? 'ctx' : `${Math.round(remainingPercent)}%`
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const normalizePromptSelection = (
|
|
211
|
+
preferredModel?: string | null,
|
|
212
|
+
preferredEffort?: ReasoningEffort | null
|
|
213
|
+
) => {
|
|
214
|
+
const withSelected = ensureModelOption(
|
|
215
|
+
availableModels.value.length > 0 ? availableModels.value : FALLBACK_MODELS,
|
|
216
|
+
preferredModel ?? selectedModel.value,
|
|
217
|
+
preferredEffort ?? selectedEffort.value
|
|
218
|
+
)
|
|
219
|
+
const visibleModels = visibleModelOptions(withSelected)
|
|
220
|
+
const nextSelection = coercePromptSelection(visibleModels, preferredModel ?? selectedModel.value, preferredEffort ?? selectedEffort.value)
|
|
221
|
+
|
|
222
|
+
availableModels.value = visibleModels
|
|
223
|
+
if (selectedModel.value !== nextSelection.model) {
|
|
224
|
+
selectedModel.value = nextSelection.model
|
|
225
|
+
}
|
|
226
|
+
if (selectedEffort.value !== nextSelection.effort) {
|
|
227
|
+
selectedEffort.value = nextSelection.effort
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const syncPromptSelectionFromThread = (
|
|
232
|
+
model: string | null | undefined,
|
|
233
|
+
effort: ReasoningEffort | null | undefined
|
|
234
|
+
) => {
|
|
235
|
+
normalizePromptSelection(model ?? null, effort ?? null)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const loadPromptControls = async () => {
|
|
239
|
+
if (promptControlsLoaded.value) {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (promptControlsPromise) {
|
|
244
|
+
return await promptControlsPromise
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
promptControlsPromise = (async () => {
|
|
248
|
+
promptControlsLoading.value = true
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
await ensureProjectRuntime()
|
|
252
|
+
const client = getClient(props.projectId)
|
|
253
|
+
const initialModel = selectedModel.value
|
|
254
|
+
const initialEffort = selectedEffort.value
|
|
255
|
+
let nextModels = availableModels.value.length > 0 ? availableModels.value : FALLBACK_MODELS
|
|
256
|
+
let defaultModel: string | null = null
|
|
257
|
+
let defaultEffort: ReasoningEffort | null = null
|
|
258
|
+
|
|
259
|
+
const [modelsResponse, configResponse] = await Promise.allSettled([
|
|
260
|
+
client.request<ModelListResponse>('model/list'),
|
|
261
|
+
client.request<ConfigReadResponse>('config/read')
|
|
262
|
+
])
|
|
263
|
+
|
|
264
|
+
if (modelsResponse.status === 'fulfilled') {
|
|
265
|
+
nextModels = visibleModelOptions(normalizeModelList(modelsResponse.value))
|
|
266
|
+
} else {
|
|
267
|
+
nextModels = visibleModelOptions(nextModels)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (configResponse.status === 'fulfilled') {
|
|
271
|
+
const defaults = normalizeConfigDefaults(configResponse.value)
|
|
272
|
+
if (defaults.contextWindow != null) {
|
|
273
|
+
modelContextWindow.value = defaults.contextWindow
|
|
274
|
+
}
|
|
275
|
+
defaultModel = defaults.model
|
|
276
|
+
defaultEffort = defaults.effort
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
availableModels.value = nextModels
|
|
280
|
+
|
|
281
|
+
const preferredModel = selectedModel.value !== initialModel
|
|
282
|
+
? selectedModel.value
|
|
283
|
+
: defaultModel ?? initialModel
|
|
284
|
+
const preferredEffort = selectedEffort.value !== initialEffort
|
|
285
|
+
? selectedEffort.value
|
|
286
|
+
: defaultEffort ?? initialEffort
|
|
287
|
+
|
|
288
|
+
normalizePromptSelection(preferredModel, preferredEffort)
|
|
289
|
+
promptControlsLoaded.value = true
|
|
290
|
+
} finally {
|
|
291
|
+
promptControlsLoading.value = false
|
|
292
|
+
promptControlsPromise = null
|
|
293
|
+
}
|
|
294
|
+
})()
|
|
295
|
+
|
|
296
|
+
return await promptControlsPromise
|
|
297
|
+
}
|
|
298
|
+
|
|
101
299
|
const subagentBootstrapPromises = new Map<string, Promise<void>>()
|
|
300
|
+
const optimisticAttachmentSnapshots = new Map<string, DraftAttachment[]>()
|
|
301
|
+
let promptControlsPromise: Promise<void> | null = null
|
|
102
302
|
|
|
103
303
|
const isActiveTurnStatus = (value: string | null | undefined) => {
|
|
104
304
|
if (!value) {
|
|
@@ -113,13 +313,249 @@ const currentLiveStream = () =>
|
|
|
113
313
|
? session.liveStream
|
|
114
314
|
: null
|
|
115
315
|
|
|
116
|
-
const
|
|
117
|
-
session.
|
|
118
|
-
|
|
316
|
+
const hasActiveTurnEngagement = () =>
|
|
317
|
+
Boolean(currentLiveStream() || session.pendingLiveStream)
|
|
318
|
+
|
|
319
|
+
const rejectLiveStreamTurnWaiters = (liveStream: LiveStream, error: Error) => {
|
|
320
|
+
const waiters = liveStream.turnIdWaiters.splice(0, liveStream.turnIdWaiters.length)
|
|
321
|
+
for (const waiter of waiters) {
|
|
322
|
+
waiter.reject(error)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const createLiveStreamState = (
|
|
327
|
+
threadId: string,
|
|
328
|
+
bufferedNotifications: CodexRpcNotification[] = []
|
|
329
|
+
): LiveStream => ({
|
|
330
|
+
threadId,
|
|
331
|
+
turnId: null,
|
|
332
|
+
bufferedNotifications,
|
|
333
|
+
observedSubagentThreadIds: new Set<string>(),
|
|
334
|
+
pendingUserMessageIds: [],
|
|
335
|
+
turnIdWaiters: [],
|
|
336
|
+
interruptRequested: false,
|
|
337
|
+
interruptAcknowledged: false,
|
|
338
|
+
unsubscribe: null
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
const setSessionLiveStream = (liveStream: LiveStream | null) => {
|
|
342
|
+
session.liveStream = liveStream
|
|
343
|
+
interruptRequested.value = liveStream?.interruptRequested === true
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const setLiveStreamInterruptRequested = (liveStream: LiveStream, nextValue: boolean) => {
|
|
347
|
+
liveStream.interruptRequested = nextValue
|
|
348
|
+
|
|
349
|
+
if (session.liveStream === liveStream) {
|
|
350
|
+
interruptRequested.value = nextValue
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const setLiveStreamTurnId = (liveStream: LiveStream, turnId: string | null) => {
|
|
355
|
+
liveStream.turnId = turnId
|
|
356
|
+
|
|
357
|
+
if (!turnId) {
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
liveStream.interruptAcknowledged = false
|
|
362
|
+
const waiters = liveStream.turnIdWaiters.splice(0, liveStream.turnIdWaiters.length)
|
|
363
|
+
for (const waiter of waiters) {
|
|
364
|
+
waiter.resolve(turnId)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const waitForLiveStreamTurnId = async (liveStream: LiveStream) => {
|
|
369
|
+
if (liveStream.turnId) {
|
|
370
|
+
return liveStream.turnId
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return await new Promise<string>((resolve, reject) => {
|
|
374
|
+
liveStream.turnIdWaiters.push({ resolve, reject })
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const clearLiveStream = (reason?: Error) => {
|
|
379
|
+
const liveStream = session.liveStream
|
|
380
|
+
if (!liveStream) {
|
|
381
|
+
interruptRequested.value = false
|
|
382
|
+
return null
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
liveStream.unsubscribe?.()
|
|
386
|
+
rejectLiveStreamTurnWaiters(liveStream, reason ?? new Error('The active turn is no longer available.'))
|
|
387
|
+
setSessionLiveStream(null)
|
|
388
|
+
return liveStream
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const ensurePendingLiveStream = async () => {
|
|
392
|
+
const existingLiveStream = currentLiveStream()
|
|
393
|
+
if (existingLiveStream) {
|
|
394
|
+
return existingLiveStream
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (session.pendingLiveStream) {
|
|
398
|
+
return await session.pendingLiveStream
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const pendingLiveStream = (async () => {
|
|
402
|
+
await ensureProjectRuntime()
|
|
403
|
+
const { threadId, created } = await ensureThread()
|
|
404
|
+
const client = getClient(props.projectId)
|
|
405
|
+
const buffered: CodexRpcNotification[] = []
|
|
406
|
+
clearLiveStream()
|
|
407
|
+
|
|
408
|
+
const liveStream = createLiveStreamState(threadId, buffered)
|
|
409
|
+
liveStream.unsubscribe = client.subscribe((notification) => {
|
|
410
|
+
const targetThreadId = notificationThreadId(notification)
|
|
411
|
+
if (!targetThreadId) {
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (targetThreadId !== threadId) {
|
|
416
|
+
if (liveStream.observedSubagentThreadIds.has(targetThreadId)) {
|
|
417
|
+
applySubagentNotification(targetThreadId, notification)
|
|
418
|
+
}
|
|
419
|
+
return
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!liveStream.turnId) {
|
|
423
|
+
buffered.push(notification)
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const turnId = notificationTurnId(notification)
|
|
428
|
+
if (turnId && turnId !== liveStream.turnId) {
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
applyNotification(notification)
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
setSessionLiveStream(liveStream)
|
|
436
|
+
|
|
437
|
+
if (created && !routeThreadId.value) {
|
|
438
|
+
pendingThreadId.value = threadId
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return liveStream
|
|
442
|
+
})()
|
|
443
|
+
|
|
444
|
+
session.pendingLiveStream = pendingLiveStream
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
return await pendingLiveStream
|
|
448
|
+
} finally {
|
|
449
|
+
if (session.pendingLiveStream === pendingLiveStream) {
|
|
450
|
+
session.pendingLiveStream = null
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const createOptimisticMessageId = () =>
|
|
456
|
+
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
457
|
+
? `local-user-${crypto.randomUUID()}`
|
|
458
|
+
: `local-user-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
459
|
+
|
|
460
|
+
const buildOptimisticMessage = (text: string, submittedAttachments: DraftAttachment[]): ChatMessage => ({
|
|
461
|
+
id: createOptimisticMessageId(),
|
|
462
|
+
role: 'user',
|
|
463
|
+
parts: [
|
|
464
|
+
...(text.trim()
|
|
465
|
+
? [{
|
|
466
|
+
type: 'text' as const,
|
|
467
|
+
text,
|
|
468
|
+
state: 'done' as const
|
|
469
|
+
}]
|
|
470
|
+
: []),
|
|
471
|
+
...submittedAttachments.map((attachment) => ({
|
|
472
|
+
type: 'attachment' as const,
|
|
473
|
+
attachment: {
|
|
474
|
+
kind: 'image' as const,
|
|
475
|
+
name: attachment.name,
|
|
476
|
+
mediaType: attachment.mediaType,
|
|
477
|
+
url: attachment.previewUrl
|
|
478
|
+
}
|
|
479
|
+
}))
|
|
480
|
+
]
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
const rememberOptimisticAttachments = (messageId: string, submittedAttachments: DraftAttachment[]) => {
|
|
484
|
+
if (submittedAttachments.length === 0) {
|
|
485
|
+
return
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
optimisticAttachmentSnapshots.set(messageId, submittedAttachments)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const formatAttachmentSize = (size: number) => {
|
|
492
|
+
if (size >= 1024 * 1024) {
|
|
493
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (size >= 1024) {
|
|
497
|
+
return `${Math.round(size / 1024)} KB`
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return `${size} B`
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const removeOptimisticMessage = (messageId: string) => {
|
|
504
|
+
messages.value = removeChatMessage(messages.value, messageId)
|
|
505
|
+
optimisticAttachmentSnapshots.delete(messageId)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const restoreDraftIfPristine = (text: string, submittedAttachments: DraftAttachment[]) => {
|
|
509
|
+
if (!input.value.trim()) {
|
|
510
|
+
input.value = text
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (attachments.value.length === 0 && submittedAttachments.length > 0) {
|
|
514
|
+
replaceAttachments(submittedAttachments)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const untrackPendingUserMessage = (messageId: string) => {
|
|
519
|
+
const liveStream = currentLiveStream()
|
|
520
|
+
if (!liveStream) {
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
liveStream.pendingUserMessageIds = removePendingUserMessageId(liveStream.pendingUserMessageIds, messageId)
|
|
119
525
|
}
|
|
120
526
|
|
|
121
|
-
const
|
|
122
|
-
|
|
527
|
+
const reconcilePendingUserMessage = (confirmedMessage: ChatMessage) => {
|
|
528
|
+
const liveStream = currentLiveStream()
|
|
529
|
+
const optimisticMessageId = liveStream?.pendingUserMessageIds.shift() ?? null
|
|
530
|
+
if (!optimisticMessageId) {
|
|
531
|
+
messages.value = upsertStreamingMessage(messages.value, confirmedMessage)
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const pendingAttachments = optimisticAttachmentSnapshots.get(optimisticMessageId)
|
|
536
|
+
if (pendingAttachments) {
|
|
537
|
+
discardSnapshot(pendingAttachments)
|
|
538
|
+
optimisticAttachmentSnapshots.delete(optimisticMessageId)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
messages.value = reconcileOptimisticUserMessage(messages.value, optimisticMessageId, confirmedMessage)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const clearPendingOptimisticMessages = (liveStream: LiveStream | null, options?: { discardSnapshots?: boolean }) => {
|
|
545
|
+
if (!liveStream) {
|
|
546
|
+
return
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
for (const messageId of liveStream.pendingUserMessageIds) {
|
|
550
|
+
messages.value = removeChatMessage(messages.value, messageId)
|
|
551
|
+
const pendingAttachments = optimisticAttachmentSnapshots.get(messageId)
|
|
552
|
+
if (options?.discardSnapshots && pendingAttachments) {
|
|
553
|
+
discardSnapshot(pendingAttachments)
|
|
554
|
+
}
|
|
555
|
+
optimisticAttachmentSnapshots.delete(messageId)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
liveStream.pendingUserMessageIds = []
|
|
123
559
|
}
|
|
124
560
|
|
|
125
561
|
const resolveThreadTitle = (thread: { name: string | null, preview: string, id: string }) => {
|
|
@@ -249,22 +685,21 @@ const hydrateThread = async (threadId: string) => {
|
|
|
249
685
|
const requestVersion = loadVersion.value + 1
|
|
250
686
|
loadVersion.value = requestVersion
|
|
251
687
|
error.value = null
|
|
688
|
+
tokenUsage.value = null
|
|
252
689
|
|
|
253
690
|
try {
|
|
254
691
|
await ensureProjectRuntime()
|
|
255
692
|
const client = getClient(props.projectId)
|
|
256
693
|
activeThreadId.value = threadId
|
|
257
694
|
|
|
258
|
-
const existingLiveStream =
|
|
695
|
+
const existingLiveStream = session.liveStream
|
|
259
696
|
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
unsubscribe: null as (() => void) | null
|
|
267
|
-
}
|
|
697
|
+
if (existingLiveStream && existingLiveStream.threadId !== threadId) {
|
|
698
|
+
clearLiveStream()
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (!session.liveStream) {
|
|
702
|
+
const nextLiveStream = createLiveStreamState(threadId)
|
|
268
703
|
|
|
269
704
|
nextLiveStream.unsubscribe = client.subscribe((notification) => {
|
|
270
705
|
const targetThreadId = notificationThreadId(notification)
|
|
@@ -292,10 +727,10 @@ const hydrateThread = async (threadId: string) => {
|
|
|
292
727
|
applyNotification(notification)
|
|
293
728
|
})
|
|
294
729
|
|
|
295
|
-
|
|
730
|
+
setSessionLiveStream(nextLiveStream)
|
|
296
731
|
}
|
|
297
732
|
|
|
298
|
-
await client.request<ThreadResumeResponse>('thread/resume', {
|
|
733
|
+
const resumeResponse = await client.request<ThreadResumeResponse>('thread/resume', {
|
|
299
734
|
threadId,
|
|
300
735
|
cwd: selectedProject.value?.projectPath ?? null,
|
|
301
736
|
approvalPolicy: 'never',
|
|
@@ -310,6 +745,10 @@ const hydrateThread = async (threadId: string) => {
|
|
|
310
745
|
return
|
|
311
746
|
}
|
|
312
747
|
|
|
748
|
+
syncPromptSelectionFromThread(
|
|
749
|
+
resumeResponse.model ?? null,
|
|
750
|
+
(resumeResponse.reasoningEffort as ReasoningEffort | null | undefined) ?? null
|
|
751
|
+
)
|
|
313
752
|
activeThreadId.value = response.thread.id
|
|
314
753
|
threadTitle.value = resolveThreadTitle(response.thread)
|
|
315
754
|
messages.value = threadToMessages(response.thread)
|
|
@@ -327,7 +766,7 @@ const hydrateThread = async (threadId: string) => {
|
|
|
327
766
|
return
|
|
328
767
|
}
|
|
329
768
|
|
|
330
|
-
session.liveStream
|
|
769
|
+
setLiveStreamTurnId(session.liveStream, activeTurn.id)
|
|
331
770
|
status.value = 'streaming'
|
|
332
771
|
|
|
333
772
|
const pendingNotifications = session.liveStream.bufferedNotifications.splice(0, session.liveStream.bufferedNotifications.length)
|
|
@@ -348,6 +787,7 @@ const hydrateThread = async (threadId: string) => {
|
|
|
348
787
|
|
|
349
788
|
const resetDraftThread = () => {
|
|
350
789
|
clearLiveStream()
|
|
790
|
+
session.pendingLiveStream = null
|
|
351
791
|
activeThreadId.value = null
|
|
352
792
|
threadTitle.value = null
|
|
353
793
|
pendingThreadId.value = null
|
|
@@ -355,6 +795,7 @@ const resetDraftThread = () => {
|
|
|
355
795
|
messages.value = []
|
|
356
796
|
subagentPanels.value = []
|
|
357
797
|
error.value = null
|
|
798
|
+
tokenUsage.value = null
|
|
358
799
|
status.value = 'ready'
|
|
359
800
|
}
|
|
360
801
|
|
|
@@ -947,6 +1388,11 @@ const applySubagentNotification = (threadId: string, notification: CodexRpcNotif
|
|
|
947
1388
|
}
|
|
948
1389
|
|
|
949
1390
|
const applyNotification = (notification: CodexRpcNotification) => {
|
|
1391
|
+
const liveStream = currentLiveStream()
|
|
1392
|
+
if (liveStream?.interruptAcknowledged && shouldIgnoreNotificationAfterInterrupt(notification.method)) {
|
|
1393
|
+
return
|
|
1394
|
+
}
|
|
1395
|
+
|
|
950
1396
|
switch (notification.method) {
|
|
951
1397
|
case 'thread/started': {
|
|
952
1398
|
const nextThreadId = notificationThreadId(notification)
|
|
@@ -959,6 +1405,10 @@ const applyNotification = (notification: CodexRpcNotification) => {
|
|
|
959
1405
|
return
|
|
960
1406
|
}
|
|
961
1407
|
case 'turn/started': {
|
|
1408
|
+
if (liveStream) {
|
|
1409
|
+
setLiveStreamTurnId(liveStream, notificationTurnId(notification))
|
|
1410
|
+
setLiveStreamInterruptRequested(liveStream, false)
|
|
1411
|
+
}
|
|
962
1412
|
messages.value = upsertStreamingMessage(
|
|
963
1413
|
messages.value,
|
|
964
1414
|
eventToMessage(`event-turn-started-${notificationTurnId(notification) ?? Date.now()}`, {
|
|
@@ -973,6 +1423,16 @@ const applyNotification = (notification: CodexRpcNotification) => {
|
|
|
973
1423
|
if (params.item.type === 'collabAgentToolCall') {
|
|
974
1424
|
applySubagentActivityItem(params.item)
|
|
975
1425
|
}
|
|
1426
|
+
if (params.item.type === 'userMessage') {
|
|
1427
|
+
for (const nextMessage of itemToMessages(params.item)) {
|
|
1428
|
+
reconcilePendingUserMessage({
|
|
1429
|
+
...nextMessage,
|
|
1430
|
+
pending: false
|
|
1431
|
+
})
|
|
1432
|
+
}
|
|
1433
|
+
status.value = 'streaming'
|
|
1434
|
+
return
|
|
1435
|
+
}
|
|
976
1436
|
seedStreamingMessage(params.item)
|
|
977
1437
|
status.value = 'streaming'
|
|
978
1438
|
return
|
|
@@ -982,14 +1442,17 @@ const applyNotification = (notification: CodexRpcNotification) => {
|
|
|
982
1442
|
if (params.item.type === 'collabAgentToolCall') {
|
|
983
1443
|
applySubagentActivityItem(params.item)
|
|
984
1444
|
}
|
|
985
|
-
if (params.item.type === 'userMessage') {
|
|
986
|
-
stripOptimisticDraftMessages()
|
|
987
|
-
}
|
|
988
1445
|
for (const nextMessage of itemToMessages(params.item)) {
|
|
989
|
-
|
|
1446
|
+
const confirmedMessage = {
|
|
990
1447
|
...nextMessage,
|
|
991
1448
|
pending: false
|
|
992
|
-
}
|
|
1449
|
+
}
|
|
1450
|
+
if (params.item.type === 'userMessage') {
|
|
1451
|
+
reconcilePendingUserMessage(confirmedMessage)
|
|
1452
|
+
continue
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
messages.value = upsertStreamingMessage(messages.value, confirmedMessage)
|
|
993
1456
|
}
|
|
994
1457
|
return
|
|
995
1458
|
}
|
|
@@ -1120,11 +1583,32 @@ const applyNotification = (notification: CodexRpcNotification) => {
|
|
|
1120
1583
|
status.value = 'streaming'
|
|
1121
1584
|
return
|
|
1122
1585
|
}
|
|
1586
|
+
case 'thread/tokenUsage/updated': {
|
|
1587
|
+
const nextUsage = normalizeThreadTokenUsage(notification.params)
|
|
1588
|
+
if (nextUsage) {
|
|
1589
|
+
tokenUsage.value = nextUsage
|
|
1590
|
+
if (nextUsage.modelContextWindow != null) {
|
|
1591
|
+
modelContextWindow.value = nextUsage.modelContextWindow
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
return
|
|
1595
|
+
}
|
|
1596
|
+
case 'error': {
|
|
1597
|
+
const params = notification.params as { error?: { message?: string } }
|
|
1598
|
+
const messageText = params.error?.message ?? 'The stream failed.'
|
|
1599
|
+
pushEventMessage('stream.error', messageText)
|
|
1600
|
+
clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
|
|
1601
|
+
clearLiveStream(new Error(messageText))
|
|
1602
|
+
error.value = messageText
|
|
1603
|
+
status.value = 'error'
|
|
1604
|
+
return
|
|
1605
|
+
}
|
|
1123
1606
|
case 'turn/failed': {
|
|
1124
1607
|
const params = notification.params as { error?: { message?: string } }
|
|
1125
1608
|
const messageText = params.error?.message ?? 'The turn failed.'
|
|
1126
1609
|
pushEventMessage('turn.failed', messageText)
|
|
1127
|
-
|
|
1610
|
+
clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
|
|
1611
|
+
clearLiveStream(new Error(messageText))
|
|
1128
1612
|
error.value = messageText
|
|
1129
1613
|
status.value = 'error'
|
|
1130
1614
|
return
|
|
@@ -1133,12 +1617,15 @@ const applyNotification = (notification: CodexRpcNotification) => {
|
|
|
1133
1617
|
const params = notification.params as { message?: string }
|
|
1134
1618
|
const messageText = params.message ?? 'The stream failed.'
|
|
1135
1619
|
pushEventMessage('stream.error', messageText)
|
|
1136
|
-
|
|
1620
|
+
clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
|
|
1621
|
+
clearLiveStream(new Error(messageText))
|
|
1137
1622
|
error.value = messageText
|
|
1138
1623
|
status.value = 'error'
|
|
1139
1624
|
return
|
|
1140
1625
|
}
|
|
1141
1626
|
case 'turn/completed': {
|
|
1627
|
+
clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
|
|
1628
|
+
error.value = null
|
|
1142
1629
|
status.value = 'ready'
|
|
1143
1630
|
clearLiveStream()
|
|
1144
1631
|
return
|
|
@@ -1150,165 +1637,126 @@ const applyNotification = (notification: CodexRpcNotification) => {
|
|
|
1150
1637
|
|
|
1151
1638
|
const sendMessage = async () => {
|
|
1152
1639
|
const text = input.value.trim()
|
|
1153
|
-
|
|
1640
|
+
const submittedAttachments = attachments.value.slice()
|
|
1641
|
+
|
|
1642
|
+
if (!text && submittedAttachments.length === 0) {
|
|
1643
|
+
return
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
try {
|
|
1647
|
+
await loadPromptControls()
|
|
1648
|
+
} catch (caughtError) {
|
|
1649
|
+
error.value = caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
1650
|
+
status.value = 'error'
|
|
1154
1651
|
return
|
|
1155
1652
|
}
|
|
1156
1653
|
|
|
1157
1654
|
pinnedToBottom.value = true
|
|
1158
1655
|
error.value = null
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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]
|
|
1656
|
+
attachmentError.value = null
|
|
1657
|
+
const submissionMethod = resolveTurnSubmissionMethod(hasActiveTurnEngagement())
|
|
1658
|
+
if (submissionMethod === 'turn/start') {
|
|
1659
|
+
status.value = 'submitted'
|
|
1175
1660
|
}
|
|
1176
|
-
|
|
1661
|
+
input.value = ''
|
|
1662
|
+
clearAttachments({ revoke: false })
|
|
1663
|
+
const optimisticMessage = buildOptimisticMessage(text, submittedAttachments)
|
|
1664
|
+
const optimisticMessageId = optimisticMessage.id
|
|
1665
|
+
rememberOptimisticAttachments(optimisticMessageId, submittedAttachments)
|
|
1666
|
+
messages.value = [...messages.value, optimisticMessage]
|
|
1667
|
+
let startedLiveStream: LiveStream | null = null
|
|
1177
1668
|
|
|
1178
1669
|
try {
|
|
1179
|
-
await ensureProjectRuntime()
|
|
1180
|
-
const { threadId, created } = await ensureThread()
|
|
1181
1670
|
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
1671
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
|
1672
|
+
if (submissionMethod === 'turn/steer') {
|
|
1673
|
+
const liveStream = await ensurePendingLiveStream()
|
|
1674
|
+
const uploadedAttachments = await uploadAttachments(liveStream.threadId, submittedAttachments)
|
|
1675
|
+
liveStream.pendingUserMessageIds.push(optimisticMessageId)
|
|
1676
|
+
const turnId = await waitForLiveStreamTurnId(liveStream)
|
|
1677
|
+
|
|
1678
|
+
await client.request<TurnStartResponse>('turn/steer', {
|
|
1679
|
+
threadId: liveStream.threadId,
|
|
1680
|
+
expectedTurnId: turnId,
|
|
1681
|
+
input: buildTurnStartInput(text, uploadedAttachments),
|
|
1682
|
+
...buildTurnOverrides(selectedModel.value, selectedEffort.value)
|
|
1683
|
+
})
|
|
1684
|
+
return
|
|
1234
1685
|
}
|
|
1235
1686
|
|
|
1236
|
-
|
|
1687
|
+
const liveStream = await ensurePendingLiveStream()
|
|
1688
|
+
startedLiveStream = liveStream
|
|
1689
|
+
const threadId = liveStream.threadId
|
|
1690
|
+
const uploadedAttachments = await uploadAttachments(threadId, submittedAttachments)
|
|
1691
|
+
liveStream.pendingUserMessageIds.push(optimisticMessageId)
|
|
1237
1692
|
|
|
1238
|
-
const
|
|
1239
|
-
resolveCompletion = resolve
|
|
1240
|
-
rejectCompletion = (caughtError: Error) => reject(caughtError)
|
|
1241
|
-
})
|
|
1242
|
-
|
|
1243
|
-
const liveStream = {
|
|
1693
|
+
const turnStart = await client.request<TurnStartResponse>('turn/start', {
|
|
1244
1694
|
threadId,
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
unsubscribe = client.subscribe((notification) => {
|
|
1252
|
-
const targetThreadId = notificationThreadId(notification)
|
|
1253
|
-
if (!targetThreadId) {
|
|
1254
|
-
return
|
|
1255
|
-
}
|
|
1695
|
+
input: buildTurnStartInput(text, uploadedAttachments),
|
|
1696
|
+
cwd: selectedProject.value?.projectPath ?? null,
|
|
1697
|
+
approvalPolicy: 'never',
|
|
1698
|
+
...buildTurnOverrides(selectedModel.value, selectedEffort.value)
|
|
1699
|
+
})
|
|
1256
1700
|
|
|
1257
|
-
|
|
1258
|
-
if (liveStream.observedSubagentThreadIds.has(targetThreadId)) {
|
|
1259
|
-
applySubagentNotification(targetThreadId, notification)
|
|
1260
|
-
}
|
|
1261
|
-
return
|
|
1262
|
-
}
|
|
1701
|
+
setLiveStreamTurnId(liveStream, turnStart.turn.id)
|
|
1263
1702
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1703
|
+
for (const notification of liveStream.bufferedNotifications.splice(0, liveStream.bufferedNotifications.length)) {
|
|
1704
|
+
const turnId = notificationTurnId(notification)
|
|
1705
|
+
if (turnId && turnId !== liveStream.turnId) {
|
|
1706
|
+
continue
|
|
1267
1707
|
}
|
|
1268
1708
|
|
|
1269
|
-
|
|
1270
|
-
})
|
|
1271
|
-
liveStream.unsubscribe = unsubscribe
|
|
1272
|
-
session.liveStream = liveStream
|
|
1273
|
-
|
|
1274
|
-
if (created && !routeThreadId.value) {
|
|
1275
|
-
pendingThreadId.value = threadId
|
|
1709
|
+
applyNotification(notification)
|
|
1276
1710
|
}
|
|
1711
|
+
} catch (caughtError) {
|
|
1712
|
+
const messageText = caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
1277
1713
|
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
|
1714
|
+
untrackPendingUserMessage(optimisticMessageId)
|
|
1715
|
+
removeOptimisticMessage(optimisticMessageId)
|
|
1291
1716
|
|
|
1292
|
-
|
|
1293
|
-
if (
|
|
1294
|
-
|
|
1717
|
+
if (submissionMethod === 'turn/start') {
|
|
1718
|
+
if (startedLiveStream && session.liveStream === startedLiveStream) {
|
|
1719
|
+
clearPendingOptimisticMessages(clearLiveStream(new Error(messageText)))
|
|
1295
1720
|
}
|
|
1721
|
+
session.pendingLiveStream = null
|
|
1722
|
+
restoreDraftIfPristine(text, submittedAttachments)
|
|
1723
|
+
error.value = messageText
|
|
1724
|
+
status.value = 'error'
|
|
1725
|
+
return
|
|
1296
1726
|
}
|
|
1297
1727
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1728
|
+
restoreDraftIfPristine(text, submittedAttachments)
|
|
1729
|
+
error.value = messageText
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
const stopActiveTurn = async () => {
|
|
1734
|
+
if (!hasActiveTurnEngagement()) {
|
|
1735
|
+
return
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
let liveStream: LiveStream | null = null
|
|
1300
1739
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1740
|
+
try {
|
|
1741
|
+
liveStream = await ensurePendingLiveStream()
|
|
1742
|
+
if (liveStream.interruptRequested) {
|
|
1303
1743
|
return
|
|
1304
1744
|
}
|
|
1745
|
+
|
|
1746
|
+
setLiveStreamInterruptRequested(liveStream, true)
|
|
1747
|
+
error.value = null
|
|
1748
|
+
const client = getClient(props.projectId)
|
|
1749
|
+
const turnId = await waitForLiveStreamTurnId(liveStream)
|
|
1750
|
+
await client.request('turn/interrupt', {
|
|
1751
|
+
threadId: liveStream.threadId,
|
|
1752
|
+
turnId
|
|
1753
|
+
})
|
|
1754
|
+
liveStream.interruptAcknowledged = true
|
|
1305
1755
|
} catch (caughtError) {
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
session.liveStream = null
|
|
1756
|
+
if (liveStream) {
|
|
1757
|
+
setLiveStreamInterruptRequested(liveStream, false)
|
|
1309
1758
|
}
|
|
1310
1759
|
error.value = caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
1311
|
-
status.value = 'error'
|
|
1312
1760
|
}
|
|
1313
1761
|
}
|
|
1314
1762
|
|
|
@@ -1335,6 +1783,7 @@ onMounted(() => {
|
|
|
1335
1783
|
void refreshProjects()
|
|
1336
1784
|
}
|
|
1337
1785
|
|
|
1786
|
+
void loadPromptControls()
|
|
1338
1787
|
void scheduleScrollToBottom('auto')
|
|
1339
1788
|
})
|
|
1340
1789
|
|
|
@@ -1388,6 +1837,18 @@ watch(status, (nextStatus, previousStatus) => {
|
|
|
1388
1837
|
|
|
1389
1838
|
void scheduleScrollToBottom(nextStatus === 'streaming' ? 'auto' : 'smooth')
|
|
1390
1839
|
}, { flush: 'post' })
|
|
1840
|
+
|
|
1841
|
+
watch([selectedModel, availableModels], () => {
|
|
1842
|
+
const nextSelection = coercePromptSelection(effectiveModelList.value, selectedModel.value, selectedEffort.value)
|
|
1843
|
+
if (selectedModel.value !== nextSelection.model) {
|
|
1844
|
+
selectedModel.value = nextSelection.model
|
|
1845
|
+
return
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
if (selectedEffort.value !== nextSelection.effort) {
|
|
1849
|
+
selectedEffort.value = nextSelection.effort
|
|
1850
|
+
}
|
|
1851
|
+
}, { flush: 'sync' })
|
|
1391
1852
|
</script>
|
|
1392
1853
|
|
|
1393
1854
|
<template>
|
|
@@ -1461,7 +1922,10 @@ watch(status, (nextStatus, previousStatus) => {
|
|
|
1461
1922
|
compact
|
|
1462
1923
|
>
|
|
1463
1924
|
<template #content="{ message }">
|
|
1464
|
-
<MessageContent
|
|
1925
|
+
<MessageContent
|
|
1926
|
+
:message="message as ChatMessage"
|
|
1927
|
+
:project-id="projectId"
|
|
1928
|
+
/>
|
|
1465
1929
|
</template>
|
|
1466
1930
|
</UChatMessages>
|
|
1467
1931
|
</div>
|
|
@@ -1469,30 +1933,261 @@ watch(status, (nextStatus, previousStatus) => {
|
|
|
1469
1933
|
<div class="sticky bottom-0 shrink-0 border-t border-default bg-default/95 px-4 py-3 backdrop-blur md:px-6">
|
|
1470
1934
|
<div class="mx-auto w-full max-w-5xl">
|
|
1471
1935
|
<TunnelNotice class="mb-3" />
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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"
|
|
1936
|
+
<UAlert
|
|
1937
|
+
v-if="composerError"
|
|
1938
|
+
color="error"
|
|
1939
|
+
variant="soft"
|
|
1940
|
+
icon="i-lucide-circle-alert"
|
|
1941
|
+
:title="composerError"
|
|
1942
|
+
class="mb-3"
|
|
1494
1943
|
/>
|
|
1495
|
-
|
|
1944
|
+
|
|
1945
|
+
<div
|
|
1946
|
+
class="relative"
|
|
1947
|
+
@dragenter="onDragEnter"
|
|
1948
|
+
@dragleave="onDragLeave"
|
|
1949
|
+
@dragover="onDragOver"
|
|
1950
|
+
@drop="onDrop"
|
|
1951
|
+
>
|
|
1952
|
+
<div
|
|
1953
|
+
v-if="isDragging"
|
|
1954
|
+
class="pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-3xl border border-dashed border-primary/50 bg-primary/10 text-sm font-medium text-primary backdrop-blur-sm"
|
|
1955
|
+
>
|
|
1956
|
+
Drop images to attach them
|
|
1957
|
+
</div>
|
|
1958
|
+
|
|
1959
|
+
<UChatPrompt
|
|
1960
|
+
v-model="input"
|
|
1961
|
+
placeholder="Describe the change you want Codex to make"
|
|
1962
|
+
:error="submitError"
|
|
1963
|
+
:disabled="isComposerDisabled"
|
|
1964
|
+
autoresize
|
|
1965
|
+
@submit.prevent="sendMessage"
|
|
1966
|
+
@keydown.enter="onPromptEnter"
|
|
1967
|
+
@compositionstart="onCompositionStart"
|
|
1968
|
+
@compositionend="onCompositionEnd"
|
|
1969
|
+
@paste="onPaste"
|
|
1970
|
+
>
|
|
1971
|
+
<template #header>
|
|
1972
|
+
<div
|
|
1973
|
+
v-if="attachments.length"
|
|
1974
|
+
class="flex flex-wrap gap-2 pb-2"
|
|
1975
|
+
>
|
|
1976
|
+
<div
|
|
1977
|
+
v-for="attachment in attachments"
|
|
1978
|
+
:key="attachment.id"
|
|
1979
|
+
class="flex max-w-full items-center gap-2 rounded-2xl border border-default bg-elevated/35 px-2 py-1.5"
|
|
1980
|
+
>
|
|
1981
|
+
<img
|
|
1982
|
+
:src="attachment.previewUrl"
|
|
1983
|
+
:alt="attachment.name"
|
|
1984
|
+
class="size-10 rounded-xl object-cover"
|
|
1985
|
+
>
|
|
1986
|
+
<div class="min-w-0">
|
|
1987
|
+
<div class="max-w-40 truncate text-xs font-medium text-highlighted">
|
|
1988
|
+
{{ attachment.name }}
|
|
1989
|
+
</div>
|
|
1990
|
+
<div class="text-[11px] text-muted">
|
|
1991
|
+
{{ formatAttachmentSize(attachment.size) }}
|
|
1992
|
+
</div>
|
|
1993
|
+
</div>
|
|
1994
|
+
<UButton
|
|
1995
|
+
type="button"
|
|
1996
|
+
color="neutral"
|
|
1997
|
+
variant="ghost"
|
|
1998
|
+
size="xs"
|
|
1999
|
+
icon="i-lucide-x"
|
|
2000
|
+
:disabled="isComposerDisabled"
|
|
2001
|
+
class="rounded-full"
|
|
2002
|
+
:aria-label="`Remove ${attachment.name}`"
|
|
2003
|
+
@click="removeAttachment(attachment.id)"
|
|
2004
|
+
/>
|
|
2005
|
+
</div>
|
|
2006
|
+
</div>
|
|
2007
|
+
</template>
|
|
2008
|
+
|
|
2009
|
+
<UChatPromptSubmit
|
|
2010
|
+
:status="promptSubmitStatus"
|
|
2011
|
+
@stop="stopActiveTurn"
|
|
2012
|
+
/>
|
|
2013
|
+
|
|
2014
|
+
<template #footer>
|
|
2015
|
+
<input
|
|
2016
|
+
ref="fileInput"
|
|
2017
|
+
type="file"
|
|
2018
|
+
accept="image/*"
|
|
2019
|
+
class="hidden"
|
|
2020
|
+
:disabled="isComposerDisabled"
|
|
2021
|
+
@change="onFileInputChange"
|
|
2022
|
+
>
|
|
2023
|
+
|
|
2024
|
+
<div class="flex w-full flex-wrap items-center gap-2 pt-1">
|
|
2025
|
+
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
|
2026
|
+
<UButton
|
|
2027
|
+
type="button"
|
|
2028
|
+
color="neutral"
|
|
2029
|
+
variant="ghost"
|
|
2030
|
+
size="sm"
|
|
2031
|
+
icon="i-lucide-plus"
|
|
2032
|
+
:disabled="isComposerDisabled"
|
|
2033
|
+
class="size-8 shrink-0 justify-center rounded-full border border-default/70"
|
|
2034
|
+
:ui="{ leadingIcon: 'size-4', base: 'px-0' }"
|
|
2035
|
+
aria-label="Attach image"
|
|
2036
|
+
@click="openFilePicker"
|
|
2037
|
+
/>
|
|
2038
|
+
|
|
2039
|
+
<USelect
|
|
2040
|
+
v-model="selectedModel"
|
|
2041
|
+
:items="modelSelectItems"
|
|
2042
|
+
color="neutral"
|
|
2043
|
+
variant="ghost"
|
|
2044
|
+
size="sm"
|
|
2045
|
+
:loading="promptControlsLoading"
|
|
2046
|
+
:disabled="isComposerDisabled"
|
|
2047
|
+
class="min-w-0 flex-1 sm:max-w-52 sm:flex-none"
|
|
2048
|
+
:ui="{ base: 'rounded-full border border-default/70 bg-default/70', value: 'truncate', content: 'min-w-56' }"
|
|
2049
|
+
/>
|
|
2050
|
+
|
|
2051
|
+
<USelect
|
|
2052
|
+
v-model="selectedEffort"
|
|
2053
|
+
:items="effortSelectItems"
|
|
2054
|
+
color="neutral"
|
|
2055
|
+
variant="ghost"
|
|
2056
|
+
size="sm"
|
|
2057
|
+
:disabled="isComposerDisabled"
|
|
2058
|
+
class="min-w-0 flex-1 sm:max-w-36 sm:flex-none"
|
|
2059
|
+
:ui="{ base: 'rounded-full border border-default/70 bg-default/70', value: 'truncate' }"
|
|
2060
|
+
/>
|
|
2061
|
+
</div>
|
|
2062
|
+
|
|
2063
|
+
<div class="ml-auto flex shrink-0 items-center">
|
|
2064
|
+
<UPopover
|
|
2065
|
+
:content="{ side: 'top', align: 'end' }"
|
|
2066
|
+
arrow
|
|
2067
|
+
>
|
|
2068
|
+
<button
|
|
2069
|
+
type="button"
|
|
2070
|
+
class="flex h-8 shrink-0 items-center gap-2 px-0 text-left text-muted"
|
|
2071
|
+
>
|
|
2072
|
+
<span class="relative flex size-7 items-center justify-center">
|
|
2073
|
+
<svg
|
|
2074
|
+
viewBox="0 0 36 36"
|
|
2075
|
+
class="size-7 -rotate-90"
|
|
2076
|
+
aria-hidden="true"
|
|
2077
|
+
>
|
|
2078
|
+
<circle
|
|
2079
|
+
cx="18"
|
|
2080
|
+
cy="18"
|
|
2081
|
+
r="15.5"
|
|
2082
|
+
fill="none"
|
|
2083
|
+
stroke-width="3"
|
|
2084
|
+
pathLength="100"
|
|
2085
|
+
class="stroke-current text-muted/20"
|
|
2086
|
+
stroke-dasharray="100 100"
|
|
2087
|
+
/>
|
|
2088
|
+
<circle
|
|
2089
|
+
cx="18"
|
|
2090
|
+
cy="18"
|
|
2091
|
+
r="15.5"
|
|
2092
|
+
fill="none"
|
|
2093
|
+
stroke-width="3"
|
|
2094
|
+
pathLength="100"
|
|
2095
|
+
class="stroke-current text-primary"
|
|
2096
|
+
stroke-linecap="round"
|
|
2097
|
+
:stroke-dasharray="`${contextUsedPercent} 100`"
|
|
2098
|
+
/>
|
|
2099
|
+
</svg>
|
|
2100
|
+
<span class="absolute text-[9px] font-semibold text-highlighted">
|
|
2101
|
+
{{ contextIndicatorLabel }}
|
|
2102
|
+
</span>
|
|
2103
|
+
</span>
|
|
2104
|
+
|
|
2105
|
+
<span class="text-[11px] leading-none text-muted">context window</span>
|
|
2106
|
+
</button>
|
|
2107
|
+
|
|
2108
|
+
<template #content>
|
|
2109
|
+
<div class="w-72 space-y-3 p-4">
|
|
2110
|
+
<div class="space-y-1">
|
|
2111
|
+
<div class="text-xs font-semibold uppercase tracking-[0.22em] text-primary">
|
|
2112
|
+
Context Window
|
|
2113
|
+
</div>
|
|
2114
|
+
<div class="text-sm font-medium text-highlighted">
|
|
2115
|
+
{{ selectedModelOption?.displayName ?? 'Selected model' }}
|
|
2116
|
+
</div>
|
|
2117
|
+
</div>
|
|
2118
|
+
|
|
2119
|
+
<div
|
|
2120
|
+
v-if="contextWindowState.contextWindow && contextWindowState.usedTokens !== null"
|
|
2121
|
+
class="grid grid-cols-2 gap-3 text-sm"
|
|
2122
|
+
>
|
|
2123
|
+
<div class="rounded-2xl border border-default bg-elevated/35 px-3 py-2">
|
|
2124
|
+
<div class="text-[11px] uppercase tracking-[0.18em] text-muted">
|
|
2125
|
+
Remaining
|
|
2126
|
+
</div>
|
|
2127
|
+
<div class="mt-1 font-semibold text-highlighted">
|
|
2128
|
+
{{ Math.round(contextWindowState.remainingPercent ?? 0) }}%
|
|
2129
|
+
</div>
|
|
2130
|
+
<div class="text-xs text-muted">
|
|
2131
|
+
{{ formatCompactTokenCount(contextWindowState.remainingTokens ?? 0) }} tokens
|
|
2132
|
+
</div>
|
|
2133
|
+
</div>
|
|
2134
|
+
<div class="rounded-2xl border border-default bg-elevated/35 px-3 py-2">
|
|
2135
|
+
<div class="text-[11px] uppercase tracking-[0.18em] text-muted">
|
|
2136
|
+
Used
|
|
2137
|
+
</div>
|
|
2138
|
+
<div class="mt-1 font-semibold text-highlighted">
|
|
2139
|
+
{{ formatCompactTokenCount(contextWindowState.usedTokens ?? 0) }}
|
|
2140
|
+
</div>
|
|
2141
|
+
<div class="text-xs text-muted">
|
|
2142
|
+
of {{ formatCompactTokenCount(contextWindowState.contextWindow) }}
|
|
2143
|
+
</div>
|
|
2144
|
+
</div>
|
|
2145
|
+
</div>
|
|
2146
|
+
|
|
2147
|
+
<div
|
|
2148
|
+
v-else
|
|
2149
|
+
class="rounded-2xl border border-default bg-elevated/35 px-3 py-2 text-sm text-muted"
|
|
2150
|
+
>
|
|
2151
|
+
<div v-if="contextWindowState.contextWindow">
|
|
2152
|
+
Live token usage will appear after the next turn completes.
|
|
2153
|
+
</div>
|
|
2154
|
+
<div v-else>
|
|
2155
|
+
Context window details are not available from the runtime yet.
|
|
2156
|
+
</div>
|
|
2157
|
+
</div>
|
|
2158
|
+
|
|
2159
|
+
<div class="grid grid-cols-2 gap-3 text-sm">
|
|
2160
|
+
<div class="rounded-2xl border border-default bg-elevated/35 px-3 py-2">
|
|
2161
|
+
<div class="text-[11px] uppercase tracking-[0.18em] text-muted">
|
|
2162
|
+
Input
|
|
2163
|
+
</div>
|
|
2164
|
+
<div class="mt-1 font-semibold text-highlighted">
|
|
2165
|
+
{{ formatCompactTokenCount(tokenUsage?.totalInputTokens ?? 0) }}
|
|
2166
|
+
</div>
|
|
2167
|
+
<div class="text-xs text-muted">
|
|
2168
|
+
cached {{ formatCompactTokenCount(tokenUsage?.totalCachedInputTokens ?? 0) }}
|
|
2169
|
+
</div>
|
|
2170
|
+
</div>
|
|
2171
|
+
<div class="rounded-2xl border border-default bg-elevated/35 px-3 py-2">
|
|
2172
|
+
<div class="text-[11px] uppercase tracking-[0.18em] text-muted">
|
|
2173
|
+
Output
|
|
2174
|
+
</div>
|
|
2175
|
+
<div class="mt-1 font-semibold text-highlighted">
|
|
2176
|
+
{{ formatCompactTokenCount(tokenUsage?.totalOutputTokens ?? 0) }}
|
|
2177
|
+
</div>
|
|
2178
|
+
<div class="text-xs text-muted">
|
|
2179
|
+
effort {{ formatReasoningEffortLabel(selectedEffort) }}
|
|
2180
|
+
</div>
|
|
2181
|
+
</div>
|
|
2182
|
+
</div>
|
|
2183
|
+
</div>
|
|
2184
|
+
</template>
|
|
2185
|
+
</UPopover>
|
|
2186
|
+
</div>
|
|
2187
|
+
</div>
|
|
2188
|
+
</template>
|
|
2189
|
+
</UChatPrompt>
|
|
2190
|
+
</div>
|
|
1496
2191
|
</div>
|
|
1497
2192
|
</div>
|
|
1498
2193
|
</section>
|