@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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { computed, ref, type Ref } from 'vue'
|
|
2
|
+
import type { CodexRpcServerRequest } from '../../shared/codex-rpc'
|
|
3
|
+
import {
|
|
4
|
+
buildPendingUserRequestDismissResponse,
|
|
5
|
+
parsePendingUserRequest,
|
|
6
|
+
type PendingUserRequest
|
|
7
|
+
} from '../../shared/pending-user-request'
|
|
8
|
+
|
|
9
|
+
type PendingUserRequestQueueEntry = {
|
|
10
|
+
request: PendingUserRequest
|
|
11
|
+
resolve: (value: unknown) => void
|
|
12
|
+
reject: (error: unknown) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type PendingUserRequestSession = {
|
|
16
|
+
current: Ref<PendingUserRequest | null>
|
|
17
|
+
queue: PendingUserRequestQueueEntry[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sessions = new Map<string, PendingUserRequestSession>()
|
|
21
|
+
|
|
22
|
+
const sessionKey = (projectId: string, threadId: string | null) =>
|
|
23
|
+
`${projectId}::${threadId ?? '__draft__'}`
|
|
24
|
+
|
|
25
|
+
const getSession = (projectId: string, threadId: string | null): PendingUserRequestSession => {
|
|
26
|
+
const key = sessionKey(projectId, threadId)
|
|
27
|
+
const existing = sessions.get(key)
|
|
28
|
+
if (existing) {
|
|
29
|
+
return existing
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const session: PendingUserRequestSession = {
|
|
33
|
+
current: ref<PendingUserRequest | null>(null),
|
|
34
|
+
queue: []
|
|
35
|
+
}
|
|
36
|
+
sessions.set(key, session)
|
|
37
|
+
return session
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const promoteNextRequest = (session: PendingUserRequestSession) => {
|
|
41
|
+
session.current.value = session.queue[0]?.request ?? null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const resolveQueuedRequest = (session: PendingUserRequestSession, responseFactory: (request: PendingUserRequest) => unknown) => {
|
|
45
|
+
while (session.queue.length > 0) {
|
|
46
|
+
const entry = session.queue.shift()
|
|
47
|
+
if (!entry) {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
entry.resolve(responseFactory(entry.request))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
session.current.value = null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const usePendingUserRequest = (projectId: string, activeThreadId: Ref<string | null>) => {
|
|
58
|
+
const resolveSession = (threadId: string | null) => getSession(projectId, threadId)
|
|
59
|
+
|
|
60
|
+
const handleServerRequest = async (request: CodexRpcServerRequest) => {
|
|
61
|
+
const normalized = parsePendingUserRequest(request)
|
|
62
|
+
if (!normalized) {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const session = resolveSession(normalized.threadId ?? activeThreadId.value)
|
|
67
|
+
|
|
68
|
+
return await new Promise((resolve, reject) => {
|
|
69
|
+
session.queue.push({
|
|
70
|
+
request: normalized,
|
|
71
|
+
resolve,
|
|
72
|
+
reject
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
if (!session.current.value) {
|
|
76
|
+
promoteNextRequest(session)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const resolveCurrentRequest = (response: unknown) => {
|
|
82
|
+
const session = resolveSession(activeThreadId.value)
|
|
83
|
+
const current = session.queue.shift()
|
|
84
|
+
if (!current) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
current.resolve(response)
|
|
89
|
+
promoteNextRequest(session)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rejectCurrentRequest = (error: unknown) => {
|
|
93
|
+
const session = resolveSession(activeThreadId.value)
|
|
94
|
+
const current = session.queue.shift()
|
|
95
|
+
if (!current) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
current.reject(error)
|
|
100
|
+
promoteNextRequest(session)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const cancelAllPendingRequests = () => {
|
|
104
|
+
const projectSessionPrefix = `${projectId}::`
|
|
105
|
+
|
|
106
|
+
for (const [key, session] of sessions.entries()) {
|
|
107
|
+
if (!key.startsWith(projectSessionPrefix)) {
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
resolveQueuedRequest(session, buildPendingUserRequestDismissResponse)
|
|
112
|
+
sessions.delete(key)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
pendingRequest: computed(() => resolveSession(activeThreadId.value).current.value),
|
|
118
|
+
hasPendingRequest: computed(() => resolveSession(activeThreadId.value).current.value !== null),
|
|
119
|
+
handleServerRequest,
|
|
120
|
+
resolveCurrentRequest,
|
|
121
|
+
rejectCurrentRequest,
|
|
122
|
+
cancelAllPendingRequests
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -23,8 +23,34 @@ type UseThreadSummariesResult = ThreadSummariesState & {
|
|
|
23
23
|
|
|
24
24
|
const states = new Map<string, ThreadSummariesState>()
|
|
25
25
|
|
|
26
|
+
export const normalizeThreadTitleCandidate = (value: string | null | undefined) => {
|
|
27
|
+
const raw = value?.trim() ?? ''
|
|
28
|
+
if (!raw) {
|
|
29
|
+
return ''
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const stripped = raw
|
|
33
|
+
.replace(/<\/?[a-z_]+>/gi, ' ')
|
|
34
|
+
.replace(/\s+/g, ' ')
|
|
35
|
+
.trim()
|
|
36
|
+
|
|
37
|
+
if (!stripped) {
|
|
38
|
+
return ''
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
/<(?:user_action|context)>/i.test(raw)
|
|
43
|
+
|| /user initiated a review task/i.test(stripped)
|
|
44
|
+
|| /review output from reviewer mode/i.test(stripped)
|
|
45
|
+
) {
|
|
46
|
+
return 'Code Review'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return stripped
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
export const resolveThreadSummaryTitle = (thread: Pick<CodexThread, 'id' | 'name' | 'preview'>) => {
|
|
27
|
-
const nextTitle = thread.name
|
|
53
|
+
const nextTitle = normalizeThreadTitleCandidate(thread.name) || normalizeThreadTitleCandidate(thread.preview)
|
|
28
54
|
return nextTitle || `Thread ${thread.id}`
|
|
29
55
|
}
|
|
30
56
|
|
|
@@ -41,7 +67,7 @@ export const renameThreadSummary = (
|
|
|
41
67
|
updatedAt?: number
|
|
42
68
|
}
|
|
43
69
|
) => {
|
|
44
|
-
const nextTitle = input.title
|
|
70
|
+
const nextTitle = normalizeThreadTitleCandidate(input.title)
|
|
45
71
|
if (!nextTitle) {
|
|
46
72
|
return threads
|
|
47
73
|
}
|
package/app/pages/index.vue
CHANGED
|
@@ -31,9 +31,10 @@ export const shouldSubmitViaTurnSteer = (input: {
|
|
|
31
31
|
liveStreamTurnId: string | null
|
|
32
32
|
status: PromptSubmitStatus
|
|
33
33
|
}) =>
|
|
34
|
-
input.activeThreadId !== null
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
(input.activeThreadId !== null
|
|
35
|
+
&& input.liveStreamThreadId === input.activeThreadId
|
|
36
|
+
&& input.status === 'submitted')
|
|
37
|
+
|| hasSteerableTurn(input)
|
|
37
38
|
|
|
38
39
|
export const shouldAwaitThreadHydration = (input: {
|
|
39
40
|
hasPendingThreadHydration: boolean
|
|
@@ -43,7 +44,26 @@ export const shouldAwaitThreadHydration = (input: {
|
|
|
43
44
|
&& input.routeThreadId !== null
|
|
44
45
|
|
|
45
46
|
export const shouldRetrySteerWithTurnStart = (message: string) =>
|
|
46
|
-
/no active turn to steer/i.test(message)
|
|
47
|
+
/no active turn to steer|active turn is no longer available/i.test(message)
|
|
48
|
+
|
|
49
|
+
export const shouldApplyNotificationToCurrentTurn = (input: {
|
|
50
|
+
liveStreamTurnId: string | null
|
|
51
|
+
lockedTurnId?: string | null
|
|
52
|
+
notificationMethod: string
|
|
53
|
+
notificationTurnId: string | null
|
|
54
|
+
}) =>
|
|
55
|
+
(input.lockedTurnId ?? input.liveStreamTurnId) === null
|
|
56
|
+
|| input.notificationTurnId === null
|
|
57
|
+
|| input.notificationTurnId === (input.lockedTurnId ?? input.liveStreamTurnId)
|
|
58
|
+
|| input.notificationMethod === 'turn/started'
|
|
59
|
+
|
|
60
|
+
export const shouldAdvanceLiveStreamTurn = (input: {
|
|
61
|
+
lockedTurnId?: string | null
|
|
62
|
+
nextTurnId: string | null
|
|
63
|
+
}) =>
|
|
64
|
+
!input.lockedTurnId
|
|
65
|
+
|| !input.nextTurnId
|
|
66
|
+
|| input.nextTurnId === input.lockedTurnId
|
|
47
67
|
|
|
48
68
|
export const resolvePromptSubmitStatus = (input: {
|
|
49
69
|
status: PromptSubmitStatus
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ComarkElement, ComarkNode, ComarkPlugin, ComarkTree } from '@comark/vue'
|
|
2
|
+
|
|
3
|
+
export const REVIEW_PRIORITY_BADGE_TAG = 'review-priority-badge'
|
|
4
|
+
|
|
5
|
+
const REVIEW_PRIORITY_PATTERN = /^P([1-3])$/i
|
|
6
|
+
|
|
7
|
+
const isElementNode = (node: ComarkNode): node is ComarkElement => {
|
|
8
|
+
return Array.isArray(node) && typeof node[0] === 'string'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const getPriorityLabel = (node: ComarkElement) => {
|
|
12
|
+
if (node[0] !== 'span') {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const children = node.slice(2)
|
|
17
|
+
|
|
18
|
+
if (children.length !== 1 || typeof children[0] !== 'string') {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const label = children[0].trim().toUpperCase()
|
|
23
|
+
|
|
24
|
+
return REVIEW_PRIORITY_PATTERN.test(label) ? label as `P${1 | 2 | 3}` : null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const createPriorityBadgeNode = (label: `P${1 | 2 | 3}`): ComarkElement => {
|
|
28
|
+
return [REVIEW_PRIORITY_BADGE_TAG, { priority: label.slice(1) }, label]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const replaceLeadingPriorityBadge = (children: ComarkNode[]) => {
|
|
32
|
+
const targetIndex = children.findIndex((child) => {
|
|
33
|
+
return typeof child !== 'string' || child.trim().length > 0
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if (targetIndex === -1) {
|
|
37
|
+
return children
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const target = children[targetIndex]
|
|
41
|
+
|
|
42
|
+
if (!target) {
|
|
43
|
+
return children
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!isElementNode(target)) {
|
|
47
|
+
return children
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const label = getPriorityLabel(target)
|
|
51
|
+
|
|
52
|
+
if (!label) {
|
|
53
|
+
return children
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return children.map((child, index) => {
|
|
57
|
+
return index === targetIndex ? createPriorityBadgeNode(label) : child
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const transformNode = (node: ComarkNode, parentTag?: string): ComarkNode => {
|
|
62
|
+
if (!isElementNode(node)) {
|
|
63
|
+
return node
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const [tag, props, ...children] = node
|
|
67
|
+
let nextChildren = children.map(child => transformNode(child, tag))
|
|
68
|
+
|
|
69
|
+
if (tag === 'li' || (tag === 'p' && parentTag === 'li')) {
|
|
70
|
+
nextChildren = replaceLeadingPriorityBadge(nextChildren)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return [tag, props, ...nextChildren]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const transformReviewPriorityBadges = (tree: ComarkTree): ComarkTree => {
|
|
77
|
+
return {
|
|
78
|
+
...tree,
|
|
79
|
+
nodes: tree.nodes.map(node => transformNode(node))
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const reviewPriorityBadgePlugin = (): ComarkPlugin => {
|
|
84
|
+
return {
|
|
85
|
+
name: 'review-priority-badge',
|
|
86
|
+
post: (state) => {
|
|
87
|
+
state.tree = transformReviewPriorityBadges(state.tree)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codori/client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Codori Nuxt dashboard for project browsing, Codex chat, and thread resume.",
|
|
6
6
|
"type": "module",
|
|
@@ -41,10 +41,17 @@
|
|
|
41
41
|
"@comark/vue": "^0.2.0",
|
|
42
42
|
"@iconify-json/lucide": "^1.2.87",
|
|
43
43
|
"@nuxt/ui": "^4.6.0",
|
|
44
|
+
"beautiful-mermaid": "^1.1.3",
|
|
45
|
+
"katex": "^0.16.45",
|
|
44
46
|
"nuxt": "^4.3.0",
|
|
45
47
|
"ofetch": "^1.4.1",
|
|
46
48
|
"shiki": "^3.23.0",
|
|
47
49
|
"tailwindcss": "^4.1.14",
|
|
48
50
|
"vue": "^3.5.22"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@vitejs/plugin-vue": "^6.0.6",
|
|
54
|
+
"@vue/test-utils": "^2.4.6",
|
|
55
|
+
"jsdom": "^29.0.2"
|
|
49
56
|
}
|
|
50
57
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineEventHandler, getRouterParam } from 'h3'
|
|
2
|
+
import { encodeProjectIdSegment, type ProjectGitBranchesResponse } from '~~/shared/codori'
|
|
3
|
+
import { proxyServerRequest } from '../../../../../utils/server-proxy'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const projectId = getRouterParam(event, 'projectId')
|
|
7
|
+
if (!projectId) {
|
|
8
|
+
throw new Error('Missing project id.')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return await proxyServerRequest<ProjectGitBranchesResponse>(
|
|
12
|
+
event,
|
|
13
|
+
`/api/projects/${encodeProjectIdSegment(projectId)}/git/branches`
|
|
14
|
+
)
|
|
15
|
+
})
|
package/shared/codex-chat.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CodexThread, CodexThreadItem, CodexUserInput } from './codex-rpc'
|
|
1
|
+
import type { CodexThread, CodexThreadItem, CodexTurn, CodexUserInput } from './codex-rpc'
|
|
2
2
|
|
|
3
3
|
export const EVENT_PART = 'data-thread-event' as const
|
|
4
4
|
export const ITEM_PART = 'data-thread-item' as const
|
|
@@ -8,6 +8,13 @@ export type ThreadEventData =
|
|
|
8
8
|
kind: 'thread.started' | 'thread.ended' | 'thread.title' | 'turn.started' | 'turn.completed'
|
|
9
9
|
title?: string | null
|
|
10
10
|
}
|
|
11
|
+
| {
|
|
12
|
+
kind: 'review.started'
|
|
13
|
+
summary: string | null
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
kind: 'review.completed'
|
|
17
|
+
}
|
|
11
18
|
| {
|
|
12
19
|
kind: 'turn.failed'
|
|
13
20
|
error: {
|
|
@@ -161,6 +168,29 @@ const userInputToParts = (input: CodexUserInput): ChatPart[] => {
|
|
|
161
168
|
}]
|
|
162
169
|
}
|
|
163
170
|
|
|
171
|
+
const getUserMessageText = (item: Extract<CodexThreadItem, { type: 'userMessage' }>) =>
|
|
172
|
+
item.content
|
|
173
|
+
.filter((input): input is Extract<CodexUserInput, { type: 'text' }> => input.type === 'text')
|
|
174
|
+
.map(input => input.text.trim())
|
|
175
|
+
.filter(Boolean)
|
|
176
|
+
.join('\n')
|
|
177
|
+
|
|
178
|
+
const shouldHideReviewBootstrapUserMessage = (
|
|
179
|
+
item: Extract<CodexThreadItem, { type: 'userMessage' }>,
|
|
180
|
+
turn: CodexTurn
|
|
181
|
+
) => {
|
|
182
|
+
const reviewLifecycle = turn.items.find((candidate): candidate is Extract<CodexThreadItem, { type: 'enteredReviewMode' | 'exitedReviewMode' }> =>
|
|
183
|
+
(candidate.type === 'enteredReviewMode' || candidate.type === 'exitedReviewMode')
|
|
184
|
+
&& candidate.id === item.id
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if (!reviewLifecycle) {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return getUserMessageText(item) === reviewLifecycle.review.trim()
|
|
192
|
+
}
|
|
193
|
+
|
|
164
194
|
export const itemToMessages = (item: CodexThreadItem): ChatMessage[] => {
|
|
165
195
|
switch (item.type) {
|
|
166
196
|
case 'userMessage':
|
|
@@ -300,13 +330,53 @@ export const itemToMessages = (item: CodexThreadItem): ChatMessage[] => {
|
|
|
300
330
|
}
|
|
301
331
|
}]
|
|
302
332
|
}]
|
|
333
|
+
case 'enteredReviewMode':
|
|
334
|
+
return [{
|
|
335
|
+
id: `${item.id}-review-started`,
|
|
336
|
+
role: 'system',
|
|
337
|
+
parts: [{
|
|
338
|
+
type: EVENT_PART,
|
|
339
|
+
data: {
|
|
340
|
+
kind: 'review.started',
|
|
341
|
+
summary: item.review.trim() || null
|
|
342
|
+
}
|
|
343
|
+
}]
|
|
344
|
+
}]
|
|
345
|
+
case 'exitedReviewMode': {
|
|
346
|
+
return [{
|
|
347
|
+
id: `${item.id}-review-completed`,
|
|
348
|
+
role: 'system',
|
|
349
|
+
parts: [{
|
|
350
|
+
type: EVENT_PART,
|
|
351
|
+
data: {
|
|
352
|
+
kind: 'review.completed'
|
|
353
|
+
}
|
|
354
|
+
}]
|
|
355
|
+
}, {
|
|
356
|
+
id: `${item.id}-review-output`,
|
|
357
|
+
role: 'assistant',
|
|
358
|
+
parts: [{
|
|
359
|
+
type: 'text',
|
|
360
|
+
text: item.review,
|
|
361
|
+
state: 'done'
|
|
362
|
+
}]
|
|
363
|
+
}]
|
|
364
|
+
}
|
|
303
365
|
default:
|
|
304
366
|
return []
|
|
305
367
|
}
|
|
306
368
|
}
|
|
307
369
|
|
|
308
370
|
export const threadToMessages = (thread: CodexThread) =>
|
|
309
|
-
thread.turns.flatMap(turn =>
|
|
371
|
+
thread.turns.flatMap((turn) =>
|
|
372
|
+
turn.items.flatMap((item) => {
|
|
373
|
+
if (item.type === 'userMessage' && shouldHideReviewBootstrapUserMessage(item, turn)) {
|
|
374
|
+
return []
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return itemToMessages(item)
|
|
378
|
+
})
|
|
379
|
+
)
|
|
310
380
|
|
|
311
381
|
export const eventToMessage = (id: string, data: ThreadEventData): ChatMessage => ({
|
|
312
382
|
id,
|
package/shared/codex-rpc.ts
CHANGED
|
@@ -24,13 +24,17 @@ type JsonRpcNotification = {
|
|
|
24
24
|
params?: unknown
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
type
|
|
27
|
+
export type CodexRpcServerRequest = JsonRpcRequest
|
|
28
|
+
|
|
29
|
+
type JsonRpcServerRequest = CodexRpcServerRequest
|
|
28
30
|
|
|
29
31
|
type PendingRequest = {
|
|
30
32
|
resolve: (value: unknown) => void
|
|
31
33
|
reject: (error: unknown) => void
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
export type CodexRpcServerRequestHandler = (request: CodexRpcServerRequest) => Promise<unknown> | unknown
|
|
37
|
+
|
|
34
38
|
export type CodexUserInput =
|
|
35
39
|
| {
|
|
36
40
|
type: 'text'
|
|
@@ -131,6 +135,16 @@ export type CodexThreadItem =
|
|
|
131
135
|
type: 'contextCompaction'
|
|
132
136
|
id: string
|
|
133
137
|
}
|
|
138
|
+
| {
|
|
139
|
+
type: 'enteredReviewMode'
|
|
140
|
+
id: string
|
|
141
|
+
review: string
|
|
142
|
+
}
|
|
143
|
+
| {
|
|
144
|
+
type: 'exitedReviewMode'
|
|
145
|
+
id: string
|
|
146
|
+
review: string
|
|
147
|
+
}
|
|
134
148
|
|
|
135
149
|
export type CodexTurn = {
|
|
136
150
|
id: string
|
|
@@ -186,6 +200,37 @@ export type TurnStartResponse = {
|
|
|
186
200
|
}
|
|
187
201
|
}
|
|
188
202
|
|
|
203
|
+
export type ReviewTarget =
|
|
204
|
+
| {
|
|
205
|
+
type: 'uncommittedChanges'
|
|
206
|
+
}
|
|
207
|
+
| {
|
|
208
|
+
type: 'baseBranch'
|
|
209
|
+
branch: string
|
|
210
|
+
}
|
|
211
|
+
| {
|
|
212
|
+
type: 'commit'
|
|
213
|
+
sha: string
|
|
214
|
+
title?: string | null
|
|
215
|
+
}
|
|
216
|
+
| {
|
|
217
|
+
type: 'custom'
|
|
218
|
+
instructions: string
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export type ReviewDelivery = 'inline' | 'detached'
|
|
222
|
+
|
|
223
|
+
export type ReviewStartParams = {
|
|
224
|
+
threadId: string
|
|
225
|
+
delivery?: ReviewDelivery
|
|
226
|
+
target: ReviewTarget
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export type ReviewStartResponse = {
|
|
230
|
+
reviewThreadId: string
|
|
231
|
+
turn: CodexTurn
|
|
232
|
+
}
|
|
233
|
+
|
|
189
234
|
export type ModelListResponse = {
|
|
190
235
|
data?: unknown[]
|
|
191
236
|
}
|
|
@@ -209,7 +254,12 @@ const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
209
254
|
const asError = (value: unknown) =>
|
|
210
255
|
value instanceof Error ? value : new Error(typeof value === 'string' ? value : 'Unknown RPC error.')
|
|
211
256
|
|
|
212
|
-
const toServerRequestResponse = async (
|
|
257
|
+
export const toServerRequestResponse = async (
|
|
258
|
+
request: JsonRpcServerRequest,
|
|
259
|
+
options?: {
|
|
260
|
+
handler?: CodexRpcServerRequestHandler | null
|
|
261
|
+
}
|
|
262
|
+
) => {
|
|
213
263
|
switch (request.method) {
|
|
214
264
|
case 'item/commandExecution/requestApproval':
|
|
215
265
|
case 'item/fileChange/requestApproval':
|
|
@@ -217,8 +267,20 @@ const toServerRequestResponse = async (request: JsonRpcServerRequest) => {
|
|
|
217
267
|
case 'item/permissions/requestApproval':
|
|
218
268
|
return { permissions: {}, scope: 'turn' }
|
|
219
269
|
case 'item/tool/requestUserInput':
|
|
270
|
+
if (options?.handler) {
|
|
271
|
+
const handled = await options.handler(request)
|
|
272
|
+
if (handled != null) {
|
|
273
|
+
return handled
|
|
274
|
+
}
|
|
275
|
+
}
|
|
220
276
|
return { answers: {} }
|
|
221
277
|
case 'mcpServer/elicitation/request':
|
|
278
|
+
if (options?.handler) {
|
|
279
|
+
const handled = await options.handler(request)
|
|
280
|
+
if (handled != null) {
|
|
281
|
+
return handled
|
|
282
|
+
}
|
|
283
|
+
}
|
|
222
284
|
return { action: 'decline', content: null, _meta: null }
|
|
223
285
|
case 'applyPatchApproval':
|
|
224
286
|
case 'execCommandApproval':
|
|
@@ -290,6 +352,8 @@ export class CodexRpcClient {
|
|
|
290
352
|
|
|
291
353
|
private listeners = new Set<(notification: CodexRpcNotification) => void>()
|
|
292
354
|
|
|
355
|
+
private serverRequestHandler: CodexRpcServerRequestHandler | null = null
|
|
356
|
+
|
|
293
357
|
constructor(url: string) {
|
|
294
358
|
this.url = url
|
|
295
359
|
}
|
|
@@ -301,6 +365,16 @@ export class CodexRpcClient {
|
|
|
301
365
|
}
|
|
302
366
|
}
|
|
303
367
|
|
|
368
|
+
setServerRequestHandler(handler: CodexRpcServerRequestHandler | null) {
|
|
369
|
+
this.serverRequestHandler = handler
|
|
370
|
+
|
|
371
|
+
return () => {
|
|
372
|
+
if (this.serverRequestHandler === handler) {
|
|
373
|
+
this.serverRequestHandler = null
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
304
378
|
async connect() {
|
|
305
379
|
if (this.initialized && this.socket?.readyState === WebSocket.OPEN) {
|
|
306
380
|
return
|
|
@@ -389,7 +463,9 @@ export class CodexRpcClient {
|
|
|
389
463
|
}
|
|
390
464
|
|
|
391
465
|
if ('id' in payload && 'method' in payload) {
|
|
392
|
-
const result = await toServerRequestResponse(payload
|
|
466
|
+
const result = await toServerRequestResponse(payload, {
|
|
467
|
+
handler: this.serverRequestHandler
|
|
468
|
+
})
|
|
393
469
|
this.sendRaw({
|
|
394
470
|
id: payload.id,
|
|
395
471
|
result
|
package/shared/codori.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveApiUrl, shouldUseServerProxy } from './network'
|
|
2
|
+
|
|
1
3
|
export type ProjectRuntimeStatus = 'running' | 'stopped' | 'error'
|
|
2
4
|
|
|
3
5
|
export type ProjectRecord = {
|
|
@@ -38,6 +40,11 @@ export type ServiceUpdateResponse = {
|
|
|
38
40
|
serviceUpdate: ServiceUpdateStatus
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
export type ProjectGitBranchesResponse = {
|
|
44
|
+
currentBranch: string | null
|
|
45
|
+
branches: string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
export const normalizeProjectIdParam = (value: string | string[] | undefined) => {
|
|
42
49
|
if (!value) {
|
|
43
50
|
return null
|
|
@@ -57,6 +64,19 @@ export const toProjectThreadRoute = (projectId: string, threadId: string) =>
|
|
|
57
64
|
|
|
58
65
|
export const encodeProjectIdSegment = (projectId: string) => encodeURIComponent(projectId)
|
|
59
66
|
|
|
67
|
+
export const resolveProjectGitBranchesUrl = (input: {
|
|
68
|
+
projectId: string
|
|
69
|
+
configuredBase?: string | null
|
|
70
|
+
}) => {
|
|
71
|
+
const requestPath = `/projects/${encodeProjectIdSegment(input.projectId)}/git/branches`
|
|
72
|
+
|
|
73
|
+
if (shouldUseServerProxy(input.configuredBase)) {
|
|
74
|
+
return `/api/codori${requestPath}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return resolveApiUrl(requestPath, input.configuredBase)
|
|
78
|
+
}
|
|
79
|
+
|
|
60
80
|
export const projectStatusMeta = (status: ProjectRuntimeStatus) => {
|
|
61
81
|
switch (status) {
|
|
62
82
|
case 'running':
|