@codori/client 0.0.6 → 0.0.8
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/assets/css/main.css +22 -0
- package/app/components/BottomDrawerShell.vue +62 -0
- package/app/components/ChatWorkspace.vue +704 -110
- package/app/components/PendingUserRequestDrawer.vue +88 -0
- package/app/components/ProjectStatusDot.vue +1 -2
- package/app/components/ReviewStartDrawer.vue +186 -0
- package/app/components/message-part/Event.vue +42 -8
- package/app/components/message-part/ReviewPriorityBadge.vue +38 -0
- package/app/components/message-part/Text.vue +128 -2
- package/app/components/pending-request/McpElicitationForm.vue +264 -0
- package/app/components/pending-request/McpElicitationUrlPrompt.vue +100 -0
- package/app/components/pending-request/RequestUserInputForm.vue +235 -0
- package/app/composables/useChatSession.ts +1 -0
- package/app/composables/usePendingUserRequest.ts +124 -0
- package/app/composables/useThreadSummaries.ts +28 -2
- package/app/pages/index.vue +0 -1
- package/app/utils/chat-turn-engagement.ts +24 -4
- package/app/utils/review-priority-badge.ts +90 -0
- package/package.json +8 -1
- package/server/api/codori/projects/[projectId]/git/branches.get.ts +15 -0
- package/shared/codex-chat.ts +72 -2
- package/shared/codex-rpc.ts +79 -3
- package/shared/codori.ts +20 -0
- package/shared/pending-user-request.ts +374 -0
- package/shared/slash-commands.ts +85 -0
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { useRouter } from '#imports'
|
|
3
|
-
import {
|
|
2
|
+
import { useRouter, useRuntimeConfig } from '#imports'
|
|
3
|
+
import {
|
|
4
|
+
computed,
|
|
5
|
+
nextTick,
|
|
6
|
+
onBeforeUnmount,
|
|
7
|
+
onMounted,
|
|
8
|
+
ref,
|
|
9
|
+
watch,
|
|
10
|
+
type ComponentPublicInstance
|
|
11
|
+
} from 'vue'
|
|
4
12
|
import MessageContent from './MessageContent.vue'
|
|
13
|
+
import ReviewStartDrawer from './ReviewStartDrawer.vue'
|
|
14
|
+
import PendingUserRequestDrawer from './PendingUserRequestDrawer.vue'
|
|
5
15
|
import {
|
|
6
16
|
reconcileOptimisticUserMessage,
|
|
7
17
|
removeChatMessage,
|
|
8
18
|
removePendingUserMessageId,
|
|
9
19
|
resolvePromptSubmitStatus,
|
|
10
20
|
resolveTurnSubmissionMethod,
|
|
21
|
+
shouldAdvanceLiveStreamTurn,
|
|
22
|
+
shouldApplyNotificationToCurrentTurn,
|
|
11
23
|
shouldSubmitViaTurnSteer,
|
|
12
24
|
shouldAwaitThreadHydration,
|
|
13
25
|
shouldRetrySteerWithTurnStart,
|
|
14
26
|
shouldIgnoreNotificationAfterInterrupt
|
|
15
27
|
} from '../utils/chat-turn-engagement'
|
|
16
28
|
import { useChatAttachments, type DraftAttachment } from '../composables/useChatAttachments'
|
|
29
|
+
import { usePendingUserRequest } from '../composables/usePendingUserRequest'
|
|
17
30
|
import { useChatSession, type LiveStream, type SubagentPanelState } from '../composables/useChatSession'
|
|
18
31
|
import { useProjects } from '../composables/useProjects'
|
|
19
32
|
import { useRpc } from '../composables/useRpc'
|
|
20
33
|
import { useChatSubmitGuard } from '../composables/useChatSubmitGuard'
|
|
21
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
normalizeThreadTitleCandidate,
|
|
36
|
+
resolveThreadSummaryTitle,
|
|
37
|
+
useThreadSummaries
|
|
38
|
+
} from '../composables/useThreadSummaries'
|
|
22
39
|
import { resolveChatMessagesStatus, shouldAwaitAssistantOutput } from '../utils/chat-messages-status'
|
|
23
40
|
import {
|
|
24
41
|
ITEM_PART,
|
|
@@ -45,6 +62,9 @@ import {
|
|
|
45
62
|
type CodexRpcNotification,
|
|
46
63
|
type CodexThread,
|
|
47
64
|
type CodexThreadItem,
|
|
65
|
+
type ReviewStartParams,
|
|
66
|
+
type ReviewStartResponse,
|
|
67
|
+
type ReviewTarget,
|
|
48
68
|
type ThreadReadResponse,
|
|
49
69
|
type ThreadResumeResponse,
|
|
50
70
|
type ThreadStartResponse,
|
|
@@ -66,7 +86,19 @@ import {
|
|
|
66
86
|
visibleModelOptions,
|
|
67
87
|
type ReasoningEffort
|
|
68
88
|
} from '~~/shared/chat-prompt-controls'
|
|
69
|
-
import {
|
|
89
|
+
import {
|
|
90
|
+
resolveProjectGitBranchesUrl,
|
|
91
|
+
toProjectThreadRoute,
|
|
92
|
+
type ProjectGitBranchesResponse
|
|
93
|
+
} from '~~/shared/codori'
|
|
94
|
+
import {
|
|
95
|
+
filterSlashCommands,
|
|
96
|
+
findActiveSlashCommand,
|
|
97
|
+
parseSubmittedSlashCommand,
|
|
98
|
+
SLASH_COMMANDS,
|
|
99
|
+
toSlashCommandCompletion,
|
|
100
|
+
type SlashCommandDefinition
|
|
101
|
+
} from '~~/shared/slash-commands'
|
|
70
102
|
|
|
71
103
|
const props = defineProps<{
|
|
72
104
|
projectId: string
|
|
@@ -74,6 +106,7 @@ const props = defineProps<{
|
|
|
74
106
|
}>()
|
|
75
107
|
|
|
76
108
|
const router = useRouter()
|
|
109
|
+
const runtimeConfig = useRuntimeConfig()
|
|
77
110
|
const { getClient } = useRpc()
|
|
78
111
|
const {
|
|
79
112
|
loaded,
|
|
@@ -105,8 +138,11 @@ const {
|
|
|
105
138
|
onDrop,
|
|
106
139
|
uploadAttachments
|
|
107
140
|
} = useChatAttachments(props.projectId)
|
|
108
|
-
|
|
109
141
|
const input = ref('')
|
|
142
|
+
const chatPromptRef = ref<{
|
|
143
|
+
textareaRef?: unknown
|
|
144
|
+
} | null>(null)
|
|
145
|
+
const slashDropdownRef = ref<ComponentPublicInstance | HTMLElement | null>(null)
|
|
110
146
|
const scrollViewport = ref<HTMLElement | null>(null)
|
|
111
147
|
const pinnedToBottom = ref(true)
|
|
112
148
|
const session = useChatSession(props.projectId)
|
|
@@ -129,16 +165,38 @@ const {
|
|
|
129
165
|
modelContextWindow,
|
|
130
166
|
tokenUsage
|
|
131
167
|
} = session
|
|
168
|
+
const {
|
|
169
|
+
pendingRequest,
|
|
170
|
+
hasPendingRequest,
|
|
171
|
+
handleServerRequest,
|
|
172
|
+
resolveCurrentRequest,
|
|
173
|
+
cancelAllPendingRequests
|
|
174
|
+
} = usePendingUserRequest(props.projectId, activeThreadId)
|
|
132
175
|
|
|
133
176
|
const selectedProject = computed(() => getProject(props.projectId))
|
|
134
177
|
const composerError = computed(() => attachmentError.value ?? error.value)
|
|
135
178
|
const submitError = computed(() => composerError.value ? new Error(composerError.value) : undefined)
|
|
136
179
|
const interruptRequested = ref(false)
|
|
137
180
|
const awaitingAssistantOutput = ref(false)
|
|
181
|
+
const sendMessageLocked = ref(false)
|
|
182
|
+
const reviewStartPending = ref(false)
|
|
183
|
+
const promptSelectionStart = ref(0)
|
|
184
|
+
const promptSelectionEnd = ref(0)
|
|
185
|
+
const isPromptFocused = ref(false)
|
|
186
|
+
const dismissedSlashMatchKey = ref<string | null>(null)
|
|
187
|
+
const slashHighlightIndex = ref(0)
|
|
188
|
+
const reviewDrawerOpen = ref(false)
|
|
189
|
+
const reviewDrawerMode = ref<'target' | 'branch'>('target')
|
|
190
|
+
const reviewDrawerCommandText = ref('/review')
|
|
191
|
+
const reviewBranches = ref<string[]>([])
|
|
192
|
+
const reviewCurrentBranch = ref<string | null>(null)
|
|
193
|
+
const reviewBranchesLoading = ref(false)
|
|
194
|
+
const reviewBranchesError = ref<string | null>(null)
|
|
138
195
|
const isBusy = computed(() =>
|
|
139
196
|
status.value === 'submitted'
|
|
140
197
|
|| status.value === 'streaming'
|
|
141
198
|
|| isUploading.value
|
|
199
|
+
|| reviewStartPending.value
|
|
142
200
|
)
|
|
143
201
|
const hasDraftContent = computed(() =>
|
|
144
202
|
input.value.trim().length > 0
|
|
@@ -147,6 +205,13 @@ const hasDraftContent = computed(() =>
|
|
|
147
205
|
const isComposerDisabled = computed(() =>
|
|
148
206
|
isUploading.value
|
|
149
207
|
|| interruptRequested.value
|
|
208
|
+
|| hasPendingRequest.value
|
|
209
|
+
|| reviewStartPending.value
|
|
210
|
+
)
|
|
211
|
+
const composerPlaceholder = computed(() =>
|
|
212
|
+
hasPendingRequest.value
|
|
213
|
+
? 'Respond to the pending request below to let Codex continue'
|
|
214
|
+
: 'Describe the change you want Codex to make'
|
|
150
215
|
)
|
|
151
216
|
const promptSubmitStatus = computed(() =>
|
|
152
217
|
resolvePromptSubmitStatus({
|
|
@@ -166,6 +231,60 @@ const showWelcomeState = computed(() =>
|
|
|
166
231
|
&& !isBusy.value
|
|
167
232
|
)
|
|
168
233
|
|
|
234
|
+
const slashCommands = computed(() =>
|
|
235
|
+
SLASH_COMMANDS.filter(() => !hasPendingRequest.value)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
const activeSlashMatch = computed(() =>
|
|
239
|
+
isPromptFocused.value
|
|
240
|
+
? findActiveSlashCommand(input.value, promptSelectionStart.value, promptSelectionEnd.value)
|
|
241
|
+
: null
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
const activeSlashMatchKey = computed(() => {
|
|
245
|
+
const match = activeSlashMatch.value
|
|
246
|
+
if (!match) {
|
|
247
|
+
return null
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return `${match.start}:${match.end}:${match.raw}`
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const filteredSlashCommands = computed(() =>
|
|
254
|
+
activeSlashMatch.value
|
|
255
|
+
? filterSlashCommands(slashCommands.value, activeSlashMatch.value.query)
|
|
256
|
+
: []
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
const slashDropdownOpen = computed(() =>
|
|
260
|
+
!reviewDrawerOpen.value
|
|
261
|
+
&& Boolean(activeSlashMatch.value)
|
|
262
|
+
&& activeSlashMatchKey.value !== dismissedSlashMatchKey.value
|
|
263
|
+
&& filteredSlashCommands.value.length > 0
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
const highlightedSlashCommand = computed(() =>
|
|
267
|
+
filteredSlashCommands.value[slashHighlightIndex.value] ?? filteredSlashCommands.value[0] ?? null
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
const slashPaletteGroups = computed(() => [{
|
|
271
|
+
id: 'slash-commands',
|
|
272
|
+
ignoreFilter: true,
|
|
273
|
+
items: filteredSlashCommands.value.map((command, index) => ({
|
|
274
|
+
label: `/${command.name}`,
|
|
275
|
+
description: command.description,
|
|
276
|
+
icon: command.name === 'review' ? 'i-lucide-search-check' : 'i-lucide-terminal',
|
|
277
|
+
active: index === slashHighlightIndex.value,
|
|
278
|
+
onSelect: () => {
|
|
279
|
+
void activateSlashCommand(command, command.supportsInlineArgs ? 'complete' : 'execute')
|
|
280
|
+
}
|
|
281
|
+
}))
|
|
282
|
+
}])
|
|
283
|
+
|
|
284
|
+
const reviewBaseBranches = computed(() =>
|
|
285
|
+
reviewBranches.value.filter(branch => branch !== reviewCurrentBranch.value)
|
|
286
|
+
)
|
|
287
|
+
|
|
169
288
|
const starterPrompts = computed(() => {
|
|
170
289
|
const project = projectTitle.value
|
|
171
290
|
|
|
@@ -314,6 +433,7 @@ const subagentBootstrapPromises = new Map<string, Promise<void>>()
|
|
|
314
433
|
const optimisticAttachmentSnapshots = new Map<string, DraftAttachment[]>()
|
|
315
434
|
let promptControlsPromise: Promise<void> | null = null
|
|
316
435
|
let pendingThreadHydration: Promise<void> | null = null
|
|
436
|
+
let releaseServerRequestHandler: (() => void) | null = null
|
|
317
437
|
|
|
318
438
|
const isActiveTurnStatus = (value: string | null | undefined) => {
|
|
319
439
|
if (!value) {
|
|
@@ -352,6 +472,7 @@ const createLiveStreamState = (
|
|
|
352
472
|
): LiveStream => ({
|
|
353
473
|
threadId,
|
|
354
474
|
turnId: null,
|
|
475
|
+
lockedTurnId: null,
|
|
355
476
|
bufferedNotifications,
|
|
356
477
|
observedSubagentThreadIds: new Set<string>(),
|
|
357
478
|
pendingUserMessageIds: [],
|
|
@@ -378,6 +499,7 @@ const setLiveStreamTurnId = (liveStream: LiveStream, turnId: string | null) => {
|
|
|
378
499
|
liveStream.turnId = turnId
|
|
379
500
|
|
|
380
501
|
if (!turnId) {
|
|
502
|
+
liveStream.lockedTurnId = null
|
|
381
503
|
return
|
|
382
504
|
}
|
|
383
505
|
|
|
@@ -388,6 +510,11 @@ const setLiveStreamTurnId = (liveStream: LiveStream, turnId: string | null) => {
|
|
|
388
510
|
}
|
|
389
511
|
}
|
|
390
512
|
|
|
513
|
+
const lockLiveStreamTurnId = (liveStream: LiveStream, turnId: string | null) => {
|
|
514
|
+
liveStream.lockedTurnId = turnId
|
|
515
|
+
setLiveStreamTurnId(liveStream, turnId)
|
|
516
|
+
}
|
|
517
|
+
|
|
391
518
|
const waitForLiveStreamTurnId = async (liveStream: LiveStream) => {
|
|
392
519
|
if (liveStream.turnId) {
|
|
393
520
|
return liveStream.turnId
|
|
@@ -407,9 +534,10 @@ const queuePendingUserMessage = (liveStream: LiveStream, messageId: string) => {
|
|
|
407
534
|
}
|
|
408
535
|
|
|
409
536
|
const replayBufferedNotifications = (liveStream: LiveStream) => {
|
|
537
|
+
const trackedTurnId = liveStream.lockedTurnId ?? liveStream.turnId
|
|
410
538
|
for (const notification of liveStream.bufferedNotifications.splice(0, liveStream.bufferedNotifications.length)) {
|
|
411
539
|
const turnId = notificationTurnId(notification)
|
|
412
|
-
if (turnId && turnId !==
|
|
540
|
+
if (turnId && trackedTurnId && turnId !== trackedTurnId) {
|
|
413
541
|
continue
|
|
414
542
|
}
|
|
415
543
|
|
|
@@ -467,7 +595,12 @@ const ensurePendingLiveStream = async () => {
|
|
|
467
595
|
}
|
|
468
596
|
|
|
469
597
|
const turnId = notificationTurnId(notification)
|
|
470
|
-
if (
|
|
598
|
+
if (!shouldApplyNotificationToCurrentTurn({
|
|
599
|
+
liveStreamTurnId: liveStream.turnId,
|
|
600
|
+
lockedTurnId: liveStream.lockedTurnId,
|
|
601
|
+
notificationMethod: notification.method,
|
|
602
|
+
notificationTurnId: turnId
|
|
603
|
+
})) {
|
|
471
604
|
return
|
|
472
605
|
}
|
|
473
606
|
|
|
@@ -542,6 +675,297 @@ const formatAttachmentSize = (size: number) => {
|
|
|
542
675
|
return `${size} B`
|
|
543
676
|
}
|
|
544
677
|
|
|
678
|
+
const isTextareaElement = (value: unknown): value is HTMLTextAreaElement =>
|
|
679
|
+
typeof HTMLTextAreaElement !== 'undefined'
|
|
680
|
+
&& value instanceof HTMLTextAreaElement
|
|
681
|
+
|
|
682
|
+
const getPromptTextarea = () => {
|
|
683
|
+
const exposed = chatPromptRef.value?.textareaRef
|
|
684
|
+
if (isTextareaElement(exposed)) {
|
|
685
|
+
return exposed
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (
|
|
689
|
+
typeof exposed === 'object'
|
|
690
|
+
&& exposed !== null
|
|
691
|
+
&& 'value' in exposed
|
|
692
|
+
&& isTextareaElement(exposed.value)
|
|
693
|
+
) {
|
|
694
|
+
return exposed.value
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return null
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const syncPromptSelectionFromDom = () => {
|
|
701
|
+
const textarea = getPromptTextarea()
|
|
702
|
+
promptSelectionStart.value = textarea?.selectionStart ?? input.value.length
|
|
703
|
+
promptSelectionEnd.value = textarea?.selectionEnd ?? input.value.length
|
|
704
|
+
|
|
705
|
+
if (!activeSlashMatchKey.value || activeSlashMatchKey.value !== dismissedSlashMatchKey.value) {
|
|
706
|
+
dismissedSlashMatchKey.value = null
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const focusPromptAt = async (position?: number) => {
|
|
711
|
+
await nextTick()
|
|
712
|
+
const textarea = getPromptTextarea()
|
|
713
|
+
if (!textarea) {
|
|
714
|
+
return
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const nextPosition = position ?? textarea.value.length
|
|
718
|
+
textarea.focus()
|
|
719
|
+
textarea.setSelectionRange(nextPosition, nextPosition)
|
|
720
|
+
promptSelectionStart.value = nextPosition
|
|
721
|
+
promptSelectionEnd.value = nextPosition
|
|
722
|
+
isPromptFocused.value = true
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const resolveSlashDropdownElement = () => {
|
|
726
|
+
const target = slashDropdownRef.value
|
|
727
|
+
if (target instanceof HTMLElement) {
|
|
728
|
+
return target
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const rootElement = target?.$el
|
|
732
|
+
return rootElement instanceof HTMLElement ? rootElement : null
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const shouldRetainPromptFocus = (nextFocused: EventTarget | null) =>
|
|
736
|
+
nextFocused instanceof Node
|
|
737
|
+
&& Boolean(resolveSlashDropdownElement()?.contains(nextFocused))
|
|
738
|
+
|
|
739
|
+
const handlePromptBlur = (event: FocusEvent) => {
|
|
740
|
+
if (shouldRetainPromptFocus(event.relatedTarget)) {
|
|
741
|
+
isPromptFocused.value = true
|
|
742
|
+
void focusPromptAt(promptSelectionStart.value)
|
|
743
|
+
return
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
isPromptFocused.value = false
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const handleSlashDropdownFocusIn = () => {
|
|
750
|
+
if (!slashDropdownOpen.value) {
|
|
751
|
+
return
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
void focusPromptAt(promptSelectionStart.value)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const preventSlashPaletteEntryFocus = (event: CustomEvent) => {
|
|
758
|
+
event.preventDefault()
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const setComposerError = (messageText: string) => {
|
|
762
|
+
markAwaitingAssistantOutput(false)
|
|
763
|
+
error.value = messageText
|
|
764
|
+
status.value = 'error'
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const resetReviewDrawerState = () => {
|
|
768
|
+
reviewDrawerMode.value = 'target'
|
|
769
|
+
reviewDrawerCommandText.value = '/review'
|
|
770
|
+
reviewBranchesLoading.value = false
|
|
771
|
+
reviewBranchesError.value = null
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const closeReviewDrawer = () => {
|
|
775
|
+
reviewDrawerOpen.value = false
|
|
776
|
+
resetReviewDrawerState()
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const openReviewDrawer = (commandText = '/review') => {
|
|
780
|
+
dismissedSlashMatchKey.value = null
|
|
781
|
+
reviewDrawerCommandText.value = commandText
|
|
782
|
+
reviewDrawerMode.value = 'target'
|
|
783
|
+
reviewBranchesError.value = null
|
|
784
|
+
reviewDrawerOpen.value = true
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const moveSlashHighlight = (delta: number) => {
|
|
788
|
+
if (!filteredSlashCommands.value.length) {
|
|
789
|
+
slashHighlightIndex.value = 0
|
|
790
|
+
return
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const maxIndex = filteredSlashCommands.value.length - 1
|
|
794
|
+
const nextIndex = slashHighlightIndex.value + delta
|
|
795
|
+
if (nextIndex < 0) {
|
|
796
|
+
slashHighlightIndex.value = maxIndex
|
|
797
|
+
return
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (nextIndex > maxIndex) {
|
|
801
|
+
slashHighlightIndex.value = 0
|
|
802
|
+
return
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
slashHighlightIndex.value = nextIndex
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const completeSlashCommand = async (command: SlashCommandDefinition) => {
|
|
809
|
+
const match = activeSlashMatch.value
|
|
810
|
+
if (!match) {
|
|
811
|
+
return
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
dismissedSlashMatchKey.value = null
|
|
815
|
+
const completion = toSlashCommandCompletion(command)
|
|
816
|
+
input.value = `${input.value.slice(0, match.start)}${completion}${input.value.slice(match.end)}`
|
|
817
|
+
await focusPromptAt(match.start + completion.length)
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const fetchProjectGitBranches = async () => {
|
|
821
|
+
reviewBranchesLoading.value = true
|
|
822
|
+
reviewBranchesError.value = null
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
const response = await fetch(resolveProjectGitBranchesUrl({
|
|
826
|
+
projectId: props.projectId,
|
|
827
|
+
configuredBase: String(runtimeConfig.public.serverBase ?? '')
|
|
828
|
+
}))
|
|
829
|
+
|
|
830
|
+
const body = await response.json() as ProjectGitBranchesResponse | { error?: { message?: string } }
|
|
831
|
+
if (!response.ok) {
|
|
832
|
+
throw new Error(body && typeof body === 'object' && 'error' in body
|
|
833
|
+
? body.error?.message ?? 'Failed to load local branches.'
|
|
834
|
+
: 'Failed to load local branches.')
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const result = body as ProjectGitBranchesResponse
|
|
838
|
+
reviewCurrentBranch.value = result.currentBranch
|
|
839
|
+
reviewBranches.value = result.branches
|
|
840
|
+
} finally {
|
|
841
|
+
reviewBranchesLoading.value = false
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const openBaseBranchPicker = async () => {
|
|
846
|
+
reviewDrawerMode.value = 'branch'
|
|
847
|
+
|
|
848
|
+
try {
|
|
849
|
+
await fetchProjectGitBranches()
|
|
850
|
+
if (!reviewBaseBranches.value.length) {
|
|
851
|
+
reviewBranchesError.value = 'No local base branches are available to compare against.'
|
|
852
|
+
}
|
|
853
|
+
} catch (caughtError) {
|
|
854
|
+
reviewBranchesError.value = caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const startReview = async (target: ReviewTarget) => {
|
|
859
|
+
if (reviewStartPending.value) {
|
|
860
|
+
return
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (hasPendingRequest.value || isBusy.value) {
|
|
864
|
+
setComposerError('Review can only start when the current thread is idle.')
|
|
865
|
+
return
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
reviewStartPending.value = true
|
|
869
|
+
const draftText = reviewDrawerCommandText.value
|
|
870
|
+
const submittedAttachments = attachments.value.slice()
|
|
871
|
+
input.value = ''
|
|
872
|
+
clearAttachments({ revoke: false })
|
|
873
|
+
|
|
874
|
+
try {
|
|
875
|
+
const client = getClient(props.projectId)
|
|
876
|
+
const liveStream = await ensurePendingLiveStream()
|
|
877
|
+
error.value = null
|
|
878
|
+
status.value = 'submitted'
|
|
879
|
+
tokenUsage.value = null
|
|
880
|
+
markAwaitingAssistantOutput(true)
|
|
881
|
+
|
|
882
|
+
const response = await client.request<ReviewStartResponse>('review/start', {
|
|
883
|
+
threadId: liveStream.threadId,
|
|
884
|
+
delivery: 'inline',
|
|
885
|
+
target
|
|
886
|
+
} satisfies ReviewStartParams)
|
|
887
|
+
|
|
888
|
+
for (const item of response.turn.items) {
|
|
889
|
+
if (item.type === 'userMessage') {
|
|
890
|
+
continue
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
for (const nextMessage of itemToMessages(item)) {
|
|
894
|
+
messages.value = upsertStreamingMessage(messages.value, {
|
|
895
|
+
...nextMessage,
|
|
896
|
+
pending: false
|
|
897
|
+
})
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
lockLiveStreamTurnId(liveStream, response.turn.id)
|
|
902
|
+
replayBufferedNotifications(liveStream)
|
|
903
|
+
closeReviewDrawer()
|
|
904
|
+
} catch (caughtError) {
|
|
905
|
+
restoreDraftIfPristine(draftText, submittedAttachments)
|
|
906
|
+
setComposerError(caughtError instanceof Error ? caughtError.message : String(caughtError))
|
|
907
|
+
} finally {
|
|
908
|
+
reviewStartPending.value = false
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const handleSlashCommandSubmission = async (
|
|
913
|
+
rawText: string,
|
|
914
|
+
submittedAttachments: DraftAttachment[]
|
|
915
|
+
) => {
|
|
916
|
+
const slashCommand = parseSubmittedSlashCommand(rawText)
|
|
917
|
+
if (!slashCommand) {
|
|
918
|
+
return false
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const command = slashCommands.value.find(candidate => candidate.name === slashCommand.name)
|
|
922
|
+
if (!command) {
|
|
923
|
+
return false
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (submittedAttachments.length > 0) {
|
|
927
|
+
setComposerError('Slash commands do not support image attachments yet.')
|
|
928
|
+
return true
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (!slashCommand.isBare) {
|
|
932
|
+
setComposerError('`/review` currently only supports the bare command. Choose the diff target in the review drawer.')
|
|
933
|
+
return true
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
error.value = null
|
|
937
|
+
status.value = 'ready'
|
|
938
|
+
openReviewDrawer(`/${command.name}`)
|
|
939
|
+
await focusPromptAt(rawText.trim().length)
|
|
940
|
+
return true
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const activateSlashCommand = async (
|
|
944
|
+
command: SlashCommandDefinition,
|
|
945
|
+
mode: 'complete' | 'execute'
|
|
946
|
+
) => {
|
|
947
|
+
if (mode === 'complete') {
|
|
948
|
+
await completeSlashCommand(command)
|
|
949
|
+
return
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
await handleSlashCommandSubmission(`/${command.name}`, attachments.value.slice())
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const handleReviewDrawerOpenChange = (open: boolean) => {
|
|
956
|
+
if (open) {
|
|
957
|
+
reviewDrawerOpen.value = true
|
|
958
|
+
return
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
closeReviewDrawer()
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const handleReviewDrawerBack = () => {
|
|
965
|
+
reviewDrawerMode.value = 'target'
|
|
966
|
+
reviewBranchesError.value = null
|
|
967
|
+
}
|
|
968
|
+
|
|
545
969
|
const removeOptimisticMessage = (messageId: string) => {
|
|
546
970
|
messages.value = removeChatMessage(messages.value, messageId)
|
|
547
971
|
optimisticAttachmentSnapshots.delete(messageId)
|
|
@@ -806,7 +1230,12 @@ const hydrateThread = async (threadId: string) => {
|
|
|
806
1230
|
}
|
|
807
1231
|
|
|
808
1232
|
const turnId = notificationTurnId(notification)
|
|
809
|
-
if (
|
|
1233
|
+
if (!shouldApplyNotificationToCurrentTurn({
|
|
1234
|
+
liveStreamTurnId: nextLiveStream.turnId,
|
|
1235
|
+
lockedTurnId: nextLiveStream.lockedTurnId,
|
|
1236
|
+
notificationMethod: notification.method,
|
|
1237
|
+
notificationTurnId: turnId
|
|
1238
|
+
})) {
|
|
810
1239
|
return
|
|
811
1240
|
}
|
|
812
1241
|
|
|
@@ -1511,7 +1940,7 @@ const applyNotification = (notification: CodexRpcNotification) => {
|
|
|
1511
1940
|
if (!nextThreadId || !nextThreadName) {
|
|
1512
1941
|
return
|
|
1513
1942
|
}
|
|
1514
|
-
const nextTitle = nextThreadName
|
|
1943
|
+
const nextTitle = normalizeThreadTitleCandidate(nextThreadName)
|
|
1515
1944
|
if (!nextTitle) {
|
|
1516
1945
|
return
|
|
1517
1946
|
}
|
|
@@ -1524,13 +1953,21 @@ const applyNotification = (notification: CodexRpcNotification) => {
|
|
|
1524
1953
|
return
|
|
1525
1954
|
}
|
|
1526
1955
|
case 'turn/started': {
|
|
1956
|
+
const nextTurnId = notificationTurnId(notification)
|
|
1957
|
+
if (liveStream && !shouldAdvanceLiveStreamTurn({
|
|
1958
|
+
lockedTurnId: liveStream.lockedTurnId,
|
|
1959
|
+
nextTurnId
|
|
1960
|
+
})) {
|
|
1961
|
+
return
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1527
1964
|
if (liveStream) {
|
|
1528
|
-
setLiveStreamTurnId(liveStream,
|
|
1965
|
+
setLiveStreamTurnId(liveStream, nextTurnId)
|
|
1529
1966
|
setLiveStreamInterruptRequested(liveStream, false)
|
|
1530
1967
|
}
|
|
1531
1968
|
messages.value = upsertStreamingMessage(
|
|
1532
1969
|
messages.value,
|
|
1533
|
-
eventToMessage(`event-turn-started-${
|
|
1970
|
+
eventToMessage(`event-turn-started-${nextTurnId ?? Date.now()}`, {
|
|
1534
1971
|
kind: 'turn.started'
|
|
1535
1972
|
})
|
|
1536
1973
|
)
|
|
@@ -1723,6 +2160,9 @@ const applyNotification = (notification: CodexRpcNotification) => {
|
|
|
1723
2160
|
markAwaitingAssistantOutput(false)
|
|
1724
2161
|
pushEventMessage('stream.error', messageText)
|
|
1725
2162
|
clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
|
|
2163
|
+
if (liveStream) {
|
|
2164
|
+
liveStream.lockedTurnId = null
|
|
2165
|
+
}
|
|
1726
2166
|
clearLiveStream(new Error(messageText))
|
|
1727
2167
|
error.value = messageText
|
|
1728
2168
|
status.value = 'error'
|
|
@@ -1734,6 +2174,9 @@ const applyNotification = (notification: CodexRpcNotification) => {
|
|
|
1734
2174
|
markAwaitingAssistantOutput(false)
|
|
1735
2175
|
pushEventMessage('turn.failed', messageText)
|
|
1736
2176
|
clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
|
|
2177
|
+
if (liveStream) {
|
|
2178
|
+
liveStream.lockedTurnId = null
|
|
2179
|
+
}
|
|
1737
2180
|
clearLiveStream(new Error(messageText))
|
|
1738
2181
|
error.value = messageText
|
|
1739
2182
|
status.value = 'error'
|
|
@@ -1745,6 +2188,9 @@ const applyNotification = (notification: CodexRpcNotification) => {
|
|
|
1745
2188
|
markAwaitingAssistantOutput(false)
|
|
1746
2189
|
pushEventMessage('stream.error', messageText)
|
|
1747
2190
|
clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
|
|
2191
|
+
if (liveStream) {
|
|
2192
|
+
liveStream.lockedTurnId = null
|
|
2193
|
+
}
|
|
1748
2194
|
clearLiveStream(new Error(messageText))
|
|
1749
2195
|
error.value = messageText
|
|
1750
2196
|
status.value = 'error'
|
|
@@ -1753,6 +2199,9 @@ const applyNotification = (notification: CodexRpcNotification) => {
|
|
|
1753
2199
|
case 'turn/completed': {
|
|
1754
2200
|
markAwaitingAssistantOutput(false)
|
|
1755
2201
|
clearPendingOptimisticMessages(liveStream, { discardSnapshots: true })
|
|
2202
|
+
if (liveStream) {
|
|
2203
|
+
liveStream.lockedTurnId = null
|
|
2204
|
+
}
|
|
1756
2205
|
error.value = null
|
|
1757
2206
|
status.value = 'ready'
|
|
1758
2207
|
clearLiveStream()
|
|
@@ -1764,118 +2213,137 @@ const applyNotification = (notification: CodexRpcNotification) => {
|
|
|
1764
2213
|
}
|
|
1765
2214
|
|
|
1766
2215
|
const sendMessage = async () => {
|
|
1767
|
-
|
|
2216
|
+
if (sendMessageLocked.value || hasPendingRequest.value) {
|
|
2217
|
+
return
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
sendMessageLocked.value = true
|
|
2221
|
+
const rawText = input.value
|
|
2222
|
+
const text = rawText.trim()
|
|
1768
2223
|
const submittedAttachments = attachments.value.slice()
|
|
1769
2224
|
|
|
1770
2225
|
if (!text && submittedAttachments.length === 0) {
|
|
2226
|
+
sendMessageLocked.value = false
|
|
1771
2227
|
return
|
|
1772
2228
|
}
|
|
1773
2229
|
|
|
2230
|
+
if (await handleSlashCommandSubmission(rawText, submittedAttachments)) {
|
|
2231
|
+
sendMessageLocked.value = false
|
|
2232
|
+
return
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
input.value = ''
|
|
2236
|
+
clearAttachments({ revoke: false })
|
|
2237
|
+
|
|
1774
2238
|
try {
|
|
1775
2239
|
await loadPromptControls()
|
|
1776
2240
|
} catch (caughtError) {
|
|
2241
|
+
restoreDraftIfPristine(text, submittedAttachments)
|
|
1777
2242
|
error.value = caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
1778
2243
|
status.value = 'error'
|
|
2244
|
+
sendMessageLocked.value = false
|
|
1779
2245
|
return
|
|
1780
2246
|
}
|
|
1781
2247
|
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
2248
|
+
try {
|
|
2249
|
+
if (pendingThreadHydration && shouldAwaitThreadHydration({
|
|
2250
|
+
hasPendingThreadHydration: true,
|
|
2251
|
+
routeThreadId: routeThreadId.value
|
|
2252
|
+
})) {
|
|
2253
|
+
await pendingThreadHydration
|
|
2254
|
+
}
|
|
1788
2255
|
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
let startedLiveStream: LiveStream | null = null
|
|
1804
|
-
let executedSubmissionMethod = submissionMethod
|
|
2256
|
+
pinnedToBottom.value = true
|
|
2257
|
+
error.value = null
|
|
2258
|
+
attachmentError.value = null
|
|
2259
|
+
const submissionMethod = resolveTurnSubmissionMethod(shouldSubmitWithTurnSteer())
|
|
2260
|
+
if (submissionMethod === 'turn/start') {
|
|
2261
|
+
status.value = 'submitted'
|
|
2262
|
+
}
|
|
2263
|
+
const optimisticMessage = buildOptimisticMessage(text, submittedAttachments)
|
|
2264
|
+
const optimisticMessageId = optimisticMessage.id
|
|
2265
|
+
rememberOptimisticAttachments(optimisticMessageId, submittedAttachments)
|
|
2266
|
+
messages.value = [...messages.value, optimisticMessage]
|
|
2267
|
+
markAwaitingAssistantOutput(shouldAwaitAssistantOutput(submissionMethod))
|
|
2268
|
+
let startedLiveStream: LiveStream | null = null
|
|
2269
|
+
let executedSubmissionMethod = submissionMethod
|
|
1805
2270
|
|
|
1806
|
-
|
|
1807
|
-
|
|
2271
|
+
try {
|
|
2272
|
+
const client = getClient(props.projectId)
|
|
1808
2273
|
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
try {
|
|
1815
|
-
uploadedAttachments = await uploadAttachments(liveStream.threadId, submittedAttachments)
|
|
1816
|
-
const turnId = await waitForLiveStreamTurnId(liveStream)
|
|
1817
|
-
|
|
1818
|
-
await client.request<TurnStartResponse>('turn/steer', {
|
|
1819
|
-
threadId: liveStream.threadId,
|
|
1820
|
-
expectedTurnId: turnId,
|
|
1821
|
-
input: buildTurnStartInput(text, uploadedAttachments),
|
|
1822
|
-
...buildTurnOverrides(selectedModel.value, selectedEffort.value)
|
|
1823
|
-
})
|
|
1824
|
-
tokenUsage.value = null
|
|
1825
|
-
} catch (caughtError) {
|
|
1826
|
-
const errorToHandle = caughtError instanceof Error ? caughtError : new Error(String(caughtError))
|
|
1827
|
-
if (!shouldRetrySteerWithTurnStart(errorToHandle.message)) {
|
|
1828
|
-
throw errorToHandle
|
|
1829
|
-
}
|
|
2274
|
+
if (submissionMethod === 'turn/steer') {
|
|
2275
|
+
const liveStream = await ensurePendingLiveStream()
|
|
2276
|
+
queuePendingUserMessage(liveStream, optimisticMessageId)
|
|
2277
|
+
let uploadedAttachments: PersistedProjectAttachment[] | undefined
|
|
1830
2278
|
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
setLiveStreamTurnId(liveStream, null)
|
|
1835
|
-
setLiveStreamInterruptRequested(liveStream, false)
|
|
2279
|
+
try {
|
|
2280
|
+
uploadedAttachments = await uploadAttachments(liveStream.threadId, submittedAttachments)
|
|
2281
|
+
const turnId = await waitForLiveStreamTurnId(liveStream)
|
|
1836
2282
|
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2283
|
+
await client.request<TurnStartResponse>('turn/steer', {
|
|
2284
|
+
threadId: liveStream.threadId,
|
|
2285
|
+
expectedTurnId: turnId,
|
|
2286
|
+
input: buildTurnStartInput(text, uploadedAttachments),
|
|
2287
|
+
...buildTurnOverrides(selectedModel.value, selectedEffort.value)
|
|
2288
|
+
})
|
|
2289
|
+
tokenUsage.value = null
|
|
2290
|
+
} catch (caughtError) {
|
|
2291
|
+
const errorToHandle = caughtError instanceof Error ? caughtError : new Error(String(caughtError))
|
|
2292
|
+
if (!shouldRetrySteerWithTurnStart(errorToHandle.message)) {
|
|
2293
|
+
throw errorToHandle
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
executedSubmissionMethod = 'turn/start'
|
|
2297
|
+
startedLiveStream = liveStream
|
|
2298
|
+
status.value = 'submitted'
|
|
2299
|
+
setLiveStreamTurnId(liveStream, null)
|
|
2300
|
+
setLiveStreamInterruptRequested(liveStream, false)
|
|
2301
|
+
|
|
2302
|
+
await submitTurnStart({
|
|
2303
|
+
client,
|
|
2304
|
+
liveStream,
|
|
2305
|
+
text,
|
|
2306
|
+
submittedAttachments,
|
|
2307
|
+
uploadedAttachments,
|
|
2308
|
+
optimisticMessageId,
|
|
2309
|
+
queueOptimisticMessage: false
|
|
2310
|
+
})
|
|
2311
|
+
}
|
|
2312
|
+
return
|
|
1846
2313
|
}
|
|
1847
|
-
return
|
|
1848
|
-
}
|
|
1849
2314
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
2315
|
+
const liveStream = await ensurePendingLiveStream()
|
|
2316
|
+
startedLiveStream = liveStream
|
|
2317
|
+
await submitTurnStart({
|
|
2318
|
+
client,
|
|
2319
|
+
liveStream,
|
|
2320
|
+
text,
|
|
2321
|
+
submittedAttachments,
|
|
2322
|
+
optimisticMessageId
|
|
2323
|
+
})
|
|
2324
|
+
} catch (caughtError) {
|
|
2325
|
+
const messageText = caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
1861
2326
|
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
2327
|
+
markAwaitingAssistantOutput(false)
|
|
2328
|
+
untrackPendingUserMessage(optimisticMessageId)
|
|
2329
|
+
removeOptimisticMessage(optimisticMessageId)
|
|
1865
2330
|
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2331
|
+
if (executedSubmissionMethod === 'turn/start') {
|
|
2332
|
+
if (startedLiveStream && session.liveStream === startedLiveStream) {
|
|
2333
|
+
clearPendingOptimisticMessages(clearLiveStream(new Error(messageText)))
|
|
2334
|
+
}
|
|
2335
|
+
session.pendingLiveStream = null
|
|
2336
|
+
restoreDraftIfPristine(text, submittedAttachments)
|
|
2337
|
+
error.value = messageText
|
|
2338
|
+
status.value = 'error'
|
|
2339
|
+
return
|
|
1869
2340
|
}
|
|
1870
|
-
|
|
2341
|
+
|
|
1871
2342
|
restoreDraftIfPristine(text, submittedAttachments)
|
|
1872
2343
|
error.value = messageText
|
|
1873
|
-
status.value = 'error'
|
|
1874
|
-
return
|
|
1875
2344
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
error.value = messageText
|
|
2345
|
+
} finally {
|
|
2346
|
+
sendMessageLocked.value = false
|
|
1879
2347
|
}
|
|
1880
2348
|
}
|
|
1881
2349
|
|
|
@@ -1910,7 +2378,7 @@ const stopActiveTurn = async () => {
|
|
|
1910
2378
|
}
|
|
1911
2379
|
|
|
1912
2380
|
const sendStarterPrompt = async (text: string) => {
|
|
1913
|
-
if (isBusy.value) {
|
|
2381
|
+
if (isBusy.value || hasPendingRequest.value) {
|
|
1914
2382
|
return
|
|
1915
2383
|
}
|
|
1916
2384
|
|
|
@@ -1918,16 +2386,71 @@ const sendStarterPrompt = async (text: string) => {
|
|
|
1918
2386
|
await sendMessage()
|
|
1919
2387
|
}
|
|
1920
2388
|
|
|
2389
|
+
const respondToPendingRequest = (response: unknown) => {
|
|
2390
|
+
error.value = null
|
|
2391
|
+
resolveCurrentRequest(response)
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
const onPromptKeydown = (event: KeyboardEvent) => {
|
|
2395
|
+
if (!slashDropdownOpen.value) {
|
|
2396
|
+
return
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
if (event.key === 'ArrowDown') {
|
|
2400
|
+
event.preventDefault()
|
|
2401
|
+
moveSlashHighlight(1)
|
|
2402
|
+
return
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
if (event.key === 'ArrowUp') {
|
|
2406
|
+
event.preventDefault()
|
|
2407
|
+
moveSlashHighlight(-1)
|
|
2408
|
+
return
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
if (event.key === 'Escape') {
|
|
2412
|
+
event.preventDefault()
|
|
2413
|
+
dismissedSlashMatchKey.value = activeSlashMatchKey.value
|
|
2414
|
+
isPromptFocused.value = true
|
|
2415
|
+
void focusPromptAt(promptSelectionStart.value)
|
|
2416
|
+
return
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
if (event.key === 'Tab' || event.key === ' ') {
|
|
2420
|
+
const command = highlightedSlashCommand.value
|
|
2421
|
+
if (!command || (event.key === ' ' && !command.completeOnSpace)) {
|
|
2422
|
+
return
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
event.preventDefault()
|
|
2426
|
+
void activateSlashCommand(command, 'complete')
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
1921
2430
|
const onPromptEnter = (event: KeyboardEvent) => {
|
|
1922
2431
|
if (!shouldSubmit(event)) {
|
|
1923
2432
|
return
|
|
1924
2433
|
}
|
|
1925
2434
|
|
|
2435
|
+
if (slashDropdownOpen.value) {
|
|
2436
|
+
const command = highlightedSlashCommand.value
|
|
2437
|
+
if (command) {
|
|
2438
|
+
event.preventDefault()
|
|
2439
|
+
const isExactMatch = activeSlashMatch.value?.query === command.name
|
|
2440
|
+
const action = isExactMatch && command.executeOnEnter && !command.supportsInlineArgs
|
|
2441
|
+
? 'execute'
|
|
2442
|
+
: 'complete'
|
|
2443
|
+
void activateSlashCommand(command, action)
|
|
2444
|
+
return
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
1926
2448
|
event.preventDefault()
|
|
1927
2449
|
void sendMessage()
|
|
1928
2450
|
}
|
|
1929
2451
|
|
|
1930
2452
|
onMounted(() => {
|
|
2453
|
+
releaseServerRequestHandler = getClient(props.projectId).setServerRequestHandler(handleServerRequest)
|
|
1931
2454
|
if (!loaded.value) {
|
|
1932
2455
|
void refreshProjects()
|
|
1933
2456
|
}
|
|
@@ -1936,6 +2459,12 @@ onMounted(() => {
|
|
|
1936
2459
|
void scheduleScrollToBottom('auto')
|
|
1937
2460
|
})
|
|
1938
2461
|
|
|
2462
|
+
onBeforeUnmount(() => {
|
|
2463
|
+
cancelAllPendingRequests()
|
|
2464
|
+
releaseServerRequestHandler?.()
|
|
2465
|
+
releaseServerRequestHandler = null
|
|
2466
|
+
})
|
|
2467
|
+
|
|
1939
2468
|
watch(() => props.threadId ?? null, (threadId) => {
|
|
1940
2469
|
if (!threadId) {
|
|
1941
2470
|
if (isBusy.value || pendingThreadId.value) {
|
|
@@ -1987,6 +2516,17 @@ watch(status, (nextStatus, previousStatus) => {
|
|
|
1987
2516
|
void scheduleScrollToBottom(nextStatus === 'streaming' ? 'auto' : 'smooth')
|
|
1988
2517
|
}, { flush: 'post' })
|
|
1989
2518
|
|
|
2519
|
+
watch(filteredSlashCommands, (commands) => {
|
|
2520
|
+
if (!commands.length) {
|
|
2521
|
+
slashHighlightIndex.value = 0
|
|
2522
|
+
return
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
if (slashHighlightIndex.value >= commands.length) {
|
|
2526
|
+
slashHighlightIndex.value = 0
|
|
2527
|
+
}
|
|
2528
|
+
}, { flush: 'sync' })
|
|
2529
|
+
|
|
1990
2530
|
watch([selectedModel, availableModels], () => {
|
|
1991
2531
|
const nextSelection = coercePromptSelection(effectiveModelList.value, selectedModel.value, selectedEffort.value)
|
|
1992
2532
|
if (selectedModel.value !== nextSelection.model) {
|
|
@@ -1998,6 +2538,11 @@ watch([selectedModel, availableModels], () => {
|
|
|
1998
2538
|
selectedEffort.value = nextSelection.effort
|
|
1999
2539
|
}
|
|
2000
2540
|
}, { flush: 'sync' })
|
|
2541
|
+
|
|
2542
|
+
watch(input, async () => {
|
|
2543
|
+
await nextTick()
|
|
2544
|
+
syncPromptSelectionFromDom()
|
|
2545
|
+
}, { flush: 'post' })
|
|
2001
2546
|
</script>
|
|
2002
2547
|
|
|
2003
2548
|
<template>
|
|
@@ -2077,19 +2622,11 @@ watch([selectedModel, availableModels], () => {
|
|
|
2077
2622
|
/>
|
|
2078
2623
|
</template>
|
|
2079
2624
|
<template #indicator>
|
|
2080
|
-
<
|
|
2625
|
+
<UChatShimmer
|
|
2081
2626
|
v-if="awaitingAssistantOutput"
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
name="i-lucide-loader-circle"
|
|
2086
|
-
class="size-4 animate-spin text-primary"
|
|
2087
|
-
/>
|
|
2088
|
-
<div class="flex flex-col gap-1">
|
|
2089
|
-
<UChatShimmer text="Waiting for response…" />
|
|
2090
|
-
<span class="text-xs text-toned">Real reasoning will appear separately when streaming starts.</span>
|
|
2091
|
-
</div>
|
|
2092
|
-
</div>
|
|
2627
|
+
text="Thinking..."
|
|
2628
|
+
class="px-1 py-2"
|
|
2629
|
+
/>
|
|
2093
2630
|
</template>
|
|
2094
2631
|
</UChatMessages>
|
|
2095
2632
|
</div>
|
|
@@ -2105,6 +2642,13 @@ watch([selectedModel, availableModels], () => {
|
|
|
2105
2642
|
class="mb-3"
|
|
2106
2643
|
/>
|
|
2107
2644
|
|
|
2645
|
+
<div
|
|
2646
|
+
v-if="pendingRequest"
|
|
2647
|
+
class="mb-3 rounded-2xl border border-primary/30 bg-primary/8 px-4 py-3 text-sm text-default"
|
|
2648
|
+
>
|
|
2649
|
+
Codex is waiting for the response in the drawer below. Normal chat sending is paused until you answer it.
|
|
2650
|
+
</div>
|
|
2651
|
+
|
|
2108
2652
|
<div
|
|
2109
2653
|
class="relative"
|
|
2110
2654
|
@dragenter="onDragEnter"
|
|
@@ -2119,14 +2663,44 @@ watch([selectedModel, availableModels], () => {
|
|
|
2119
2663
|
Drop images to attach them
|
|
2120
2664
|
</div>
|
|
2121
2665
|
|
|
2666
|
+
<UCommandPalette
|
|
2667
|
+
v-if="slashDropdownOpen"
|
|
2668
|
+
ref="slashDropdownRef"
|
|
2669
|
+
class="absolute bottom-full left-0 z-20 mb-2 w-[90vw] max-w-[calc(100vw-2rem)] md:w-[min(50vw,52rem)] md:max-w-[min(50vw,52rem)]"
|
|
2670
|
+
:groups="slashPaletteGroups"
|
|
2671
|
+
:input="false"
|
|
2672
|
+
:autofocus="false"
|
|
2673
|
+
:search-term="activeSlashMatch?.query ?? ''"
|
|
2674
|
+
:ui="{
|
|
2675
|
+
root: 'min-h-0 overflow-hidden rounded-2xl border border-default bg-default/95 shadow-2xl backdrop-blur',
|
|
2676
|
+
content: 'max-h-72 overflow-y-auto p-2',
|
|
2677
|
+
group: 'space-y-1',
|
|
2678
|
+
item: 'rounded-xl px-3 py-2.5',
|
|
2679
|
+
itemLeadingIcon: 'size-4',
|
|
2680
|
+
itemLabel: 'text-sm font-medium',
|
|
2681
|
+
itemDescription: 'text-xs leading-5'
|
|
2682
|
+
}"
|
|
2683
|
+
@entry-focus="preventSlashPaletteEntryFocus"
|
|
2684
|
+
@focusin.capture="handleSlashDropdownFocusIn"
|
|
2685
|
+
@mousedown.prevent
|
|
2686
|
+
/>
|
|
2687
|
+
|
|
2122
2688
|
<UChatPrompt
|
|
2689
|
+
ref="chatPromptRef"
|
|
2123
2690
|
v-model="input"
|
|
2124
|
-
placeholder="
|
|
2691
|
+
:placeholder="composerPlaceholder"
|
|
2125
2692
|
:error="submitError"
|
|
2126
2693
|
:disabled="isComposerDisabled"
|
|
2127
2694
|
autoresize
|
|
2128
2695
|
@submit.prevent="sendMessage"
|
|
2696
|
+
@keydown="onPromptKeydown"
|
|
2129
2697
|
@keydown.enter="onPromptEnter"
|
|
2698
|
+
@input="syncPromptSelectionFromDom"
|
|
2699
|
+
@click="syncPromptSelectionFromDom"
|
|
2700
|
+
@keyup="syncPromptSelectionFromDom"
|
|
2701
|
+
@focus="isPromptFocused = true; syncPromptSelectionFromDom()"
|
|
2702
|
+
@blur="handlePromptBlur"
|
|
2703
|
+
@select="syncPromptSelectionFromDom"
|
|
2130
2704
|
@compositionstart="onCompositionStart"
|
|
2131
2705
|
@compositionend="onCompositionEnd"
|
|
2132
2706
|
@paste="onPaste"
|
|
@@ -2338,4 +2912,24 @@ watch([selectedModel, availableModels], () => {
|
|
|
2338
2912
|
</div>
|
|
2339
2913
|
</div>
|
|
2340
2914
|
</section>
|
|
2915
|
+
|
|
2916
|
+
<PendingUserRequestDrawer
|
|
2917
|
+
:request="pendingRequest"
|
|
2918
|
+
@respond="respondToPendingRequest"
|
|
2919
|
+
/>
|
|
2920
|
+
|
|
2921
|
+
<ReviewStartDrawer
|
|
2922
|
+
:open="reviewDrawerOpen"
|
|
2923
|
+
:mode="reviewDrawerMode"
|
|
2924
|
+
:branches="reviewBaseBranches"
|
|
2925
|
+
:current-branch="reviewCurrentBranch"
|
|
2926
|
+
:loading="reviewBranchesLoading"
|
|
2927
|
+
:submitting="reviewStartPending"
|
|
2928
|
+
:error="reviewBranchesError"
|
|
2929
|
+
@update:open="handleReviewDrawerOpenChange"
|
|
2930
|
+
@choose-current-changes="startReview({ type: 'uncommittedChanges' })"
|
|
2931
|
+
@choose-base-branch-mode="openBaseBranchPicker"
|
|
2932
|
+
@choose-base-branch="(branch) => startReview({ type: 'baseBranch', branch })"
|
|
2933
|
+
@back="handleReviewDrawerBack"
|
|
2934
|
+
/>
|
|
2341
2935
|
</template>
|