@codori/client 0.0.2 → 0.0.4
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 +905 -183
- package/app/components/MessageContent.vue +2 -0
- package/app/components/MessagePartRenderer.ts +10 -0
- package/app/components/SubagentDrawerList.vue +64 -0
- package/app/components/SubagentTranscriptPanel.vue +305 -0
- package/app/components/ThreadPanel.vue +64 -45
- package/app/components/VisualSubagentStack.vue +13 -243
- package/app/components/message-part/Attachment.vue +61 -0
- package/app/composables/useChatAttachments.ts +208 -0
- package/app/composables/useChatSession.ts +31 -0
- package/app/composables/useProjects.ts +42 -0
- package/app/layouts/default.vue +56 -5
- package/app/pages/index.vue +0 -1
- package/app/pages/projects/[...projectId]/index.vue +2 -2
- package/app/pages/projects/[...projectId]/threads/[threadId].vue +223 -70
- 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/service/update.get.ts +7 -0
- package/server/api/codori/service/update.post.ts +9 -0
- 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 +79 -14
- package/shared/codex-rpc.ts +19 -0
- package/shared/codori.ts +12 -0
- package/shared/subagent-panels.ts +158 -0
- package/app/components/TunnelNotice.vue +0 -27
package/app/layouts/default.vue
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
|
+
import { useProjects } from '../composables/useProjects'
|
|
3
4
|
|
|
4
5
|
const sidebarCollapsed = ref(false)
|
|
6
|
+
const { serviceUpdate, serviceUpdatePending, triggerServiceUpdate } = useProjects()
|
|
7
|
+
|
|
8
|
+
const showServiceUpdateButton = computed(() =>
|
|
9
|
+
serviceUpdate.value.enabled && (serviceUpdate.value.updateAvailable || serviceUpdate.value.updating)
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
const serviceUpdateLabel = computed(() =>
|
|
13
|
+
serviceUpdate.value.updating ? 'Updating' : 'Update'
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const serviceUpdateTooltip = computed(() => {
|
|
17
|
+
if (!serviceUpdate.value.latestVersion || !serviceUpdate.value.installedVersion) {
|
|
18
|
+
return serviceUpdate.value.updating ? 'Applying the latest server package update.' : 'Install the latest @codori/server package.'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return serviceUpdate.value.updating
|
|
22
|
+
? `Updating @codori/server ${serviceUpdate.value.installedVersion} -> ${serviceUpdate.value.latestVersion}`
|
|
23
|
+
: `Update @codori/server ${serviceUpdate.value.installedVersion} -> ${serviceUpdate.value.latestVersion}`
|
|
24
|
+
})
|
|
5
25
|
|
|
6
26
|
const sidebarUi = computed(() =>
|
|
7
27
|
sidebarCollapsed.value
|
|
@@ -46,13 +66,44 @@ const sidebarUi = computed(() =>
|
|
|
46
66
|
class="size-5"
|
|
47
67
|
/>
|
|
48
68
|
</div>
|
|
49
|
-
<div
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
<div
|
|
70
|
+
v-if="!collapsed"
|
|
71
|
+
class="flex min-w-0 flex-1 items-start justify-between gap-3"
|
|
72
|
+
>
|
|
73
|
+
<div class="min-w-0">
|
|
74
|
+
<div class="text-sm font-semibold">
|
|
75
|
+
Codori
|
|
76
|
+
</div>
|
|
77
|
+
<div class="text-xs text-muted">
|
|
78
|
+
Codex project control
|
|
79
|
+
</div>
|
|
52
80
|
</div>
|
|
53
|
-
<
|
|
81
|
+
<UTooltip
|
|
82
|
+
v-if="showServiceUpdateButton"
|
|
83
|
+
:text="serviceUpdateTooltip"
|
|
84
|
+
>
|
|
85
|
+
<UButton
|
|
86
|
+
color="neutral"
|
|
87
|
+
variant="outline"
|
|
88
|
+
size="xs"
|
|
89
|
+
:loading="serviceUpdatePending || serviceUpdate.updating"
|
|
90
|
+
:disabled="serviceUpdatePending || serviceUpdate.updating"
|
|
91
|
+
@click="triggerServiceUpdate"
|
|
92
|
+
>
|
|
93
|
+
{{ serviceUpdateLabel }}
|
|
94
|
+
</UButton>
|
|
95
|
+
</UTooltip>
|
|
96
|
+
</div>
|
|
97
|
+
<div
|
|
98
|
+
v-else-if="collapsed"
|
|
99
|
+
class="sr-only"
|
|
100
|
+
>
|
|
101
|
+
<span>
|
|
102
|
+
Codori
|
|
103
|
+
</span>
|
|
104
|
+
<span>
|
|
54
105
|
Codex project control
|
|
55
|
-
</
|
|
106
|
+
</span>
|
|
56
107
|
</div>
|
|
57
108
|
</div>
|
|
58
109
|
</template>
|
package/app/pages/index.vue
CHANGED
|
@@ -7,7 +7,7 @@ import { normalizeProjectIdParam } from '~~/shared/codori'
|
|
|
7
7
|
|
|
8
8
|
const route = useRoute()
|
|
9
9
|
const router = useRouter()
|
|
10
|
-
const {
|
|
10
|
+
const { togglePanel } = useThreadPanel()
|
|
11
11
|
const { loaded, refreshProjects, getProject, pendingProjectId } = useProjects()
|
|
12
12
|
|
|
13
13
|
const projectId = computed(() => normalizeProjectIdParam(route.params.projectId as string | string[] | undefined))
|
|
@@ -83,7 +83,7 @@ onMounted(() => {
|
|
|
83
83
|
color="neutral"
|
|
84
84
|
variant="outline"
|
|
85
85
|
square
|
|
86
|
-
@click="
|
|
86
|
+
@click="togglePanel"
|
|
87
87
|
/>
|
|
88
88
|
</UTooltip>
|
|
89
89
|
</div>
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useRoute, useRouter } from '#imports'
|
|
3
|
-
import { computed, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
4
4
|
import { useChatSession } from '../../../../composables/useChatSession'
|
|
5
5
|
import { useProjects } from '../../../../composables/useProjects'
|
|
6
6
|
import { useThreadPanel } from '../../../../composables/useThreadPanel'
|
|
7
7
|
import { useVisualSubagentPanels } from '../../../../composables/useVisualSubagentPanels'
|
|
8
8
|
import { normalizeProjectIdParam, toProjectRoute } from '~~/shared/codori'
|
|
9
|
+
import {
|
|
10
|
+
pruneExpandedSubagentThreadId,
|
|
11
|
+
resolveExpandedSubagentPanel,
|
|
12
|
+
resolveSubagentAccent,
|
|
13
|
+
resolveSubagentPanelAutoOpen,
|
|
14
|
+
toSubagentAvatarText
|
|
15
|
+
} from '~~/shared/subagent-panels'
|
|
9
16
|
|
|
10
17
|
const route = useRoute()
|
|
11
18
|
const router = useRouter()
|
|
12
|
-
const {
|
|
19
|
+
const { togglePanel } = useThreadPanel()
|
|
13
20
|
const { loaded, refreshProjects, getProject, pendingProjectId } = useProjects()
|
|
14
21
|
|
|
15
22
|
const projectId = computed(() => normalizeProjectIdParam(route.params.projectId as string | string[] | undefined))
|
|
@@ -51,41 +58,81 @@ const rpcStatus = computed(() => {
|
|
|
51
58
|
}
|
|
52
59
|
})
|
|
53
60
|
const isSubagentsPanelOpen = ref(false)
|
|
61
|
+
const isMobileSubagentsDrawerOpen = ref(false)
|
|
62
|
+
const expandedSubagentThreadId = ref<string | null>(null)
|
|
54
63
|
const hasUserToggledSubagentsPanel = ref(false)
|
|
55
64
|
const hasResolvedSubagentPanelState = ref(false)
|
|
56
65
|
const previousActiveSubagentCount = ref(0)
|
|
66
|
+
const isMobileViewport = ref(true)
|
|
57
67
|
const hasAvailableSubagents = computed(() => availablePanels.value.length > 0)
|
|
58
|
-
const
|
|
59
|
-
isSubagentsPanelOpen.value && hasAvailableSubagents.value
|
|
68
|
+
const isDesktopSubagentsPanelVisible = computed(() =>
|
|
69
|
+
!isMobileViewport.value && isSubagentsPanelOpen.value && hasAvailableSubagents.value
|
|
60
70
|
)
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
const isSubagentsSurfaceOpen = computed(() =>
|
|
72
|
+
isMobileViewport.value ? isMobileSubagentsDrawerOpen.value : isDesktopSubagentsPanelVisible.value
|
|
73
|
+
)
|
|
74
|
+
const subagentsToggleIcon = computed(() => 'i-lucide-bot')
|
|
75
|
+
const subagentsToggleLabel = computed(() =>
|
|
76
|
+
isMobileViewport.value
|
|
77
|
+
? (isMobileSubagentsDrawerOpen.value ? 'Hide subagents' : 'Show subagents')
|
|
78
|
+
: (isDesktopSubagentsPanelVisible.value ? 'Hide subagents' : 'Show subagents')
|
|
65
79
|
)
|
|
66
|
-
const toSubagentAvatarText = (name: string) => {
|
|
67
|
-
const normalized = name.replace(/\s+/g, '').trim()
|
|
68
|
-
return Array.from(normalized || 'AG').slice(0, 2).join('')
|
|
69
|
-
}
|
|
70
80
|
const subagentAvatarItems = computed(() =>
|
|
71
81
|
availablePanels.value.map((panel, index) => ({
|
|
72
82
|
threadId: panel.threadId,
|
|
73
83
|
name: panel.name,
|
|
74
84
|
text: toSubagentAvatarText(panel.name),
|
|
75
|
-
class:
|
|
76
|
-
'bg-emerald-500/15 text-emerald-700 ring-1 ring-inset ring-emerald-500/30 dark:text-emerald-300',
|
|
77
|
-
'bg-sky-500/15 text-sky-700 ring-1 ring-inset ring-sky-500/30 dark:text-sky-300',
|
|
78
|
-
'bg-amber-500/15 text-amber-800 ring-1 ring-inset ring-amber-500/35 dark:text-amber-300',
|
|
79
|
-
'bg-rose-500/15 text-rose-700 ring-1 ring-inset ring-rose-500/30 dark:text-rose-300',
|
|
80
|
-
'bg-violet-500/15 text-violet-700 ring-1 ring-inset ring-violet-500/30 dark:text-violet-300'
|
|
81
|
-
][index % 5]
|
|
85
|
+
class: resolveSubagentAccent(index).avatarClass
|
|
82
86
|
}))
|
|
83
87
|
)
|
|
88
|
+
const expandedSubagentPanel = computed(() =>
|
|
89
|
+
resolveExpandedSubagentPanel(availablePanels.value, expandedSubagentThreadId.value)
|
|
90
|
+
)
|
|
91
|
+
const expandedSubagentAccent = computed(() => {
|
|
92
|
+
if (!expandedSubagentPanel.value) {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const index = availablePanels.value.findIndex(panel => panel.threadId === expandedSubagentPanel.value?.threadId)
|
|
97
|
+
return index >= 0 ? resolveSubagentAccent(index) : null
|
|
98
|
+
})
|
|
99
|
+
const isExpandedSubagentOpen = computed({
|
|
100
|
+
get: () => expandedSubagentPanel.value !== null,
|
|
101
|
+
set: (open) => {
|
|
102
|
+
if (!open) {
|
|
103
|
+
expandedSubagentThreadId.value = null
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
let viewportQuery: MediaQueryList | null = null
|
|
109
|
+
let removeViewportListener: (() => void) | null = null
|
|
110
|
+
|
|
111
|
+
const syncViewportMode = (mobile: boolean) => {
|
|
112
|
+
isMobileViewport.value = mobile
|
|
113
|
+
|
|
114
|
+
if (mobile) {
|
|
115
|
+
isSubagentsPanelOpen.value = false
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
isMobileSubagentsDrawerOpen.value = false
|
|
120
|
+
|
|
121
|
+
if (hasAvailableSubagents.value && !hasUserToggledSubagentsPanel.value && activePanels.value.length > 0) {
|
|
122
|
+
isSubagentsPanelOpen.value = true
|
|
123
|
+
}
|
|
124
|
+
}
|
|
84
125
|
|
|
85
126
|
const toggleSubagentsPanel = () => {
|
|
86
127
|
if (!hasAvailableSubagents.value) {
|
|
87
128
|
return
|
|
88
129
|
}
|
|
130
|
+
|
|
131
|
+
if (isMobileViewport.value) {
|
|
132
|
+
isMobileSubagentsDrawerOpen.value = !isMobileSubagentsDrawerOpen.value
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
89
136
|
hasUserToggledSubagentsPanel.value = true
|
|
90
137
|
isSubagentsPanelOpen.value = !isSubagentsPanelOpen.value
|
|
91
138
|
}
|
|
@@ -95,6 +142,15 @@ const closeSubagentsPanel = () => {
|
|
|
95
142
|
isSubagentsPanelOpen.value = false
|
|
96
143
|
}
|
|
97
144
|
|
|
145
|
+
const openExpandedSubagent = (subagentThreadId: string) => {
|
|
146
|
+
expandedSubagentThreadId.value = subagentThreadId
|
|
147
|
+
isMobileSubagentsDrawerOpen.value = false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const closeExpandedSubagent = () => {
|
|
151
|
+
expandedSubagentThreadId.value = null
|
|
152
|
+
}
|
|
153
|
+
|
|
98
154
|
const onNewThread = async () => {
|
|
99
155
|
if (!projectId.value) {
|
|
100
156
|
return
|
|
@@ -107,6 +163,26 @@ onMounted(() => {
|
|
|
107
163
|
if (!loaded.value) {
|
|
108
164
|
void refreshProjects()
|
|
109
165
|
}
|
|
166
|
+
|
|
167
|
+
if (!import.meta.client) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
viewportQuery = window.matchMedia('(max-width: 767px)')
|
|
172
|
+
syncViewportMode(viewportQuery.matches)
|
|
173
|
+
|
|
174
|
+
const handleViewportChange = (event: MediaQueryListEvent) => {
|
|
175
|
+
syncViewportMode(event.matches)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
viewportQuery.addEventListener('change', handleViewportChange)
|
|
179
|
+
removeViewportListener = () => {
|
|
180
|
+
viewportQuery?.removeEventListener('change', handleViewportChange)
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
onBeforeUnmount(() => {
|
|
185
|
+
removeViewportListener?.()
|
|
110
186
|
})
|
|
111
187
|
|
|
112
188
|
watch(threadId, () => {
|
|
@@ -114,6 +190,8 @@ watch(threadId, () => {
|
|
|
114
190
|
hasResolvedSubagentPanelState.value = false
|
|
115
191
|
previousActiveSubagentCount.value = 0
|
|
116
192
|
isSubagentsPanelOpen.value = false
|
|
193
|
+
isMobileSubagentsDrawerOpen.value = false
|
|
194
|
+
expandedSubagentThreadId.value = null
|
|
117
195
|
}, { immediate: true })
|
|
118
196
|
|
|
119
197
|
watch(hasAvailableSubagents, (value) => {
|
|
@@ -122,31 +200,33 @@ watch(hasAvailableSubagents, (value) => {
|
|
|
122
200
|
hasResolvedSubagentPanelState.value = false
|
|
123
201
|
previousActiveSubagentCount.value = 0
|
|
124
202
|
isSubagentsPanelOpen.value = false
|
|
203
|
+
isMobileSubagentsDrawerOpen.value = false
|
|
204
|
+
expandedSubagentThreadId.value = null
|
|
125
205
|
}
|
|
126
206
|
}, { immediate: true })
|
|
127
207
|
|
|
208
|
+
watch(availablePanels, (panels) => {
|
|
209
|
+
expandedSubagentThreadId.value = pruneExpandedSubagentThreadId(panels, expandedSubagentThreadId.value)
|
|
210
|
+
}, { immediate: true })
|
|
211
|
+
|
|
128
212
|
watch(
|
|
129
213
|
() => activePanels.value.length,
|
|
130
214
|
(nextCount) => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
215
|
+
const nextState = resolveSubagentPanelAutoOpen({
|
|
216
|
+
isMobile: isMobileViewport.value,
|
|
217
|
+
hasAvailableSubagents: hasAvailableSubagents.value,
|
|
218
|
+
hasResolvedState: hasResolvedSubagentPanelState.value,
|
|
219
|
+
hasUserToggled: hasUserToggledSubagentsPanel.value,
|
|
220
|
+
previousActiveCount: previousActiveSubagentCount.value,
|
|
221
|
+
nextActiveCount: nextCount
|
|
222
|
+
})
|
|
134
223
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
previousActiveSubagentCount.value = nextCount
|
|
138
|
-
isSubagentsPanelOpen.value = nextCount > 0
|
|
139
|
-
return
|
|
140
|
-
}
|
|
224
|
+
hasResolvedSubagentPanelState.value = nextState.hasResolvedState
|
|
225
|
+
previousActiveSubagentCount.value = nextState.previousActiveCount
|
|
141
226
|
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
&& nextCount > 0
|
|
145
|
-
) {
|
|
146
|
-
isSubagentsPanelOpen.value = true
|
|
227
|
+
if (nextState.nextOpen !== null) {
|
|
228
|
+
isSubagentsPanelOpen.value = nextState.nextOpen
|
|
147
229
|
}
|
|
148
|
-
|
|
149
|
-
previousActiveSubagentCount.value = nextCount
|
|
150
230
|
},
|
|
151
231
|
{ immediate: true }
|
|
152
232
|
)
|
|
@@ -157,10 +237,10 @@ watch(
|
|
|
157
237
|
<UDashboardPanel
|
|
158
238
|
id="thread-shell"
|
|
159
239
|
class="min-h-0 min-w-0 flex-1"
|
|
160
|
-
:default-size="
|
|
161
|
-
:min-size="
|
|
162
|
-
:max-size="
|
|
163
|
-
:resizable="
|
|
240
|
+
:default-size="isDesktopSubagentsPanelVisible ? 70 : undefined"
|
|
241
|
+
:min-size="isDesktopSubagentsPanelVisible ? 50 : undefined"
|
|
242
|
+
:max-size="isDesktopSubagentsPanelVisible ? 75 : undefined"
|
|
243
|
+
:resizable="isDesktopSubagentsPanelVisible"
|
|
164
244
|
:ui="{ root: '!p-0', body: '!p-0 sm:!p-0 !gap-0 sm:!gap-0' }"
|
|
165
245
|
>
|
|
166
246
|
<template #header>
|
|
@@ -188,17 +268,42 @@ watch(
|
|
|
188
268
|
</template>
|
|
189
269
|
<template #right>
|
|
190
270
|
<div class="flex items-center gap-1.5 lg:gap-2">
|
|
271
|
+
<UTooltip :text="`RPC ${rpcStatus}`">
|
|
272
|
+
<ProjectStatusDot
|
|
273
|
+
:status="rpcStatus"
|
|
274
|
+
pulse
|
|
275
|
+
padded
|
|
276
|
+
/>
|
|
277
|
+
</UTooltip>
|
|
278
|
+
<UTooltip text="New thread">
|
|
279
|
+
<UButton
|
|
280
|
+
icon="i-lucide-plus"
|
|
281
|
+
color="primary"
|
|
282
|
+
variant="soft"
|
|
283
|
+
square
|
|
284
|
+
@click="onNewThread"
|
|
285
|
+
/>
|
|
286
|
+
</UTooltip>
|
|
287
|
+
<UTooltip text="Previous threads">
|
|
288
|
+
<UButton
|
|
289
|
+
icon="i-lucide-history"
|
|
290
|
+
color="neutral"
|
|
291
|
+
variant="outline"
|
|
292
|
+
square
|
|
293
|
+
@click="togglePanel"
|
|
294
|
+
/>
|
|
295
|
+
</UTooltip>
|
|
191
296
|
<UTooltip
|
|
192
297
|
v-if="hasAvailableSubagents"
|
|
193
298
|
text="Subagents"
|
|
194
299
|
>
|
|
195
300
|
<UButton
|
|
196
|
-
:color="
|
|
197
|
-
:variant="
|
|
301
|
+
:color="isSubagentsSurfaceOpen ? 'primary' : 'neutral'"
|
|
302
|
+
:variant="isSubagentsSurfaceOpen ? 'soft' : 'ghost'"
|
|
198
303
|
:icon="subagentsToggleIcon"
|
|
199
304
|
size="sm"
|
|
200
305
|
class="px-2 xl:ps-2 xl:pe-2.5"
|
|
201
|
-
:aria-label="
|
|
306
|
+
:aria-label="subagentsToggleLabel"
|
|
202
307
|
@click="toggleSubagentsPanel"
|
|
203
308
|
>
|
|
204
309
|
<UAvatarGroup
|
|
@@ -217,31 +322,6 @@ watch(
|
|
|
217
322
|
</UAvatarGroup>
|
|
218
323
|
</UButton>
|
|
219
324
|
</UTooltip>
|
|
220
|
-
<UTooltip :text="`RPC ${rpcStatus}`">
|
|
221
|
-
<ProjectStatusDot
|
|
222
|
-
:status="rpcStatus"
|
|
223
|
-
pulse
|
|
224
|
-
padded
|
|
225
|
-
/>
|
|
226
|
-
</UTooltip>
|
|
227
|
-
<UTooltip text="New thread">
|
|
228
|
-
<UButton
|
|
229
|
-
icon="i-lucide-plus"
|
|
230
|
-
color="primary"
|
|
231
|
-
variant="soft"
|
|
232
|
-
square
|
|
233
|
-
@click="onNewThread"
|
|
234
|
-
/>
|
|
235
|
-
</UTooltip>
|
|
236
|
-
<UTooltip text="Previous threads">
|
|
237
|
-
<UButton
|
|
238
|
-
icon="i-lucide-history"
|
|
239
|
-
color="neutral"
|
|
240
|
-
variant="outline"
|
|
241
|
-
square
|
|
242
|
-
@click="openPanel"
|
|
243
|
-
/>
|
|
244
|
-
</UTooltip>
|
|
245
325
|
</div>
|
|
246
326
|
</template>
|
|
247
327
|
</UDashboardNavbar>
|
|
@@ -258,17 +338,20 @@ watch(
|
|
|
258
338
|
</UDashboardPanel>
|
|
259
339
|
|
|
260
340
|
<UDashboardPanel
|
|
261
|
-
v-if="
|
|
341
|
+
v-if="isDesktopSubagentsPanelVisible"
|
|
262
342
|
id="thread-subagents-panel"
|
|
263
343
|
class="h-full min-h-0"
|
|
264
344
|
:default-size="30"
|
|
265
345
|
:min-size="20"
|
|
266
346
|
:max-size="40"
|
|
267
347
|
resizable
|
|
268
|
-
:ui="{ body: '!p-0' }"
|
|
348
|
+
:ui="{ header: '!p-0 bg-[var(--ui-bg)]', body: '!p-0' }"
|
|
269
349
|
>
|
|
270
350
|
<template #header>
|
|
271
|
-
<div
|
|
351
|
+
<div
|
|
352
|
+
class="flex items-center justify-between gap-2 border-b border-default px-4 py-3"
|
|
353
|
+
style="background-color: var(--ui-bg);"
|
|
354
|
+
>
|
|
272
355
|
<div class="flex items-center gap-2">
|
|
273
356
|
<span class="text-sm font-semibold text-highlighted">Subagents</span>
|
|
274
357
|
<UBadge
|
|
@@ -294,10 +377,80 @@ watch(
|
|
|
294
377
|
<VisualSubagentStack
|
|
295
378
|
:agents="availablePanels"
|
|
296
379
|
class="h-full min-h-0"
|
|
380
|
+
@expand="openExpandedSubagent"
|
|
297
381
|
/>
|
|
298
382
|
</template>
|
|
299
383
|
</UDashboardPanel>
|
|
300
384
|
|
|
385
|
+
<UDrawer
|
|
386
|
+
v-if="hasAvailableSubagents"
|
|
387
|
+
v-model:open="isMobileSubagentsDrawerOpen"
|
|
388
|
+
direction="bottom"
|
|
389
|
+
:handle="true"
|
|
390
|
+
:ui="{
|
|
391
|
+
content: 'max-h-[85dvh] rounded-t-2xl md:hidden',
|
|
392
|
+
container: 'gap-0 p-0',
|
|
393
|
+
header: 'px-4 pb-2 pt-4',
|
|
394
|
+
body: '!p-0',
|
|
395
|
+
footer: 'hidden'
|
|
396
|
+
}"
|
|
397
|
+
>
|
|
398
|
+
<template #header>
|
|
399
|
+
<div class="flex items-center justify-between gap-2">
|
|
400
|
+
<div class="flex items-center gap-2">
|
|
401
|
+
<span class="text-sm font-semibold text-highlighted">Subagents</span>
|
|
402
|
+
<UBadge
|
|
403
|
+
color="primary"
|
|
404
|
+
variant="soft"
|
|
405
|
+
size="sm"
|
|
406
|
+
>
|
|
407
|
+
{{ availablePanels.length }}
|
|
408
|
+
</UBadge>
|
|
409
|
+
</div>
|
|
410
|
+
<UButton
|
|
411
|
+
icon="i-lucide-x"
|
|
412
|
+
color="neutral"
|
|
413
|
+
variant="ghost"
|
|
414
|
+
size="xs"
|
|
415
|
+
square
|
|
416
|
+
aria-label="Close subagents drawer"
|
|
417
|
+
@click="isMobileSubagentsDrawerOpen = false"
|
|
418
|
+
/>
|
|
419
|
+
</div>
|
|
420
|
+
</template>
|
|
421
|
+
|
|
422
|
+
<template #body>
|
|
423
|
+
<SubagentDrawerList
|
|
424
|
+
:agents="availablePanels"
|
|
425
|
+
@expand="openExpandedSubagent"
|
|
426
|
+
/>
|
|
427
|
+
</template>
|
|
428
|
+
</UDrawer>
|
|
429
|
+
|
|
430
|
+
<UModal
|
|
431
|
+
v-model:open="isExpandedSubagentOpen"
|
|
432
|
+
fullscreen
|
|
433
|
+
:ui="{
|
|
434
|
+
header: 'hidden',
|
|
435
|
+
close: 'hidden',
|
|
436
|
+
content: 'overflow-hidden bg-default',
|
|
437
|
+
body: '!h-full !p-0'
|
|
438
|
+
}"
|
|
439
|
+
>
|
|
440
|
+
<template #body>
|
|
441
|
+
<SubagentTranscriptPanel
|
|
442
|
+
v-if="expandedSubagentPanel"
|
|
443
|
+
:agent="expandedSubagentPanel"
|
|
444
|
+
:accent="expandedSubagentAccent"
|
|
445
|
+
scroll-scope="expanded"
|
|
446
|
+
expanded
|
|
447
|
+
show-collapse-button
|
|
448
|
+
class="h-full"
|
|
449
|
+
@collapse="closeExpandedSubagent"
|
|
450
|
+
/>
|
|
451
|
+
</template>
|
|
452
|
+
</UModal>
|
|
453
|
+
|
|
301
454
|
<ThreadPanel :project-id="projectId" />
|
|
302
455
|
</div>
|
|
303
456
|
</template>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { upsertStreamingMessage, type ChatMessage } from '../../shared/codex-chat'
|
|
2
|
+
|
|
3
|
+
export type PromptSubmitStatus = 'ready' | 'submitted' | 'streaming' | 'error'
|
|
4
|
+
|
|
5
|
+
const interruptIgnoredMethods = new Set([
|
|
6
|
+
'item/started',
|
|
7
|
+
'item/agentMessage/delta',
|
|
8
|
+
'item/plan/delta',
|
|
9
|
+
'item/reasoning/textDelta',
|
|
10
|
+
'item/reasoning/summaryTextDelta',
|
|
11
|
+
'item/commandExecution/outputDelta',
|
|
12
|
+
'item/fileChange/outputDelta',
|
|
13
|
+
'item/mcpToolCall/progress'
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
export const resolveTurnSubmissionMethod = (hasActiveTurn: boolean) =>
|
|
17
|
+
hasActiveTurn ? 'turn/steer' : 'turn/start'
|
|
18
|
+
|
|
19
|
+
export const resolvePromptSubmitStatus = (input: {
|
|
20
|
+
status: PromptSubmitStatus
|
|
21
|
+
hasDraftContent: boolean
|
|
22
|
+
}) => input.hasDraftContent ? 'ready' : input.status
|
|
23
|
+
|
|
24
|
+
export const shouldIgnoreNotificationAfterInterrupt = (method: string) =>
|
|
25
|
+
interruptIgnoredMethods.has(method)
|
|
26
|
+
|
|
27
|
+
export const removeChatMessage = (messages: ChatMessage[], messageId: string) =>
|
|
28
|
+
messages.filter(message => message.id !== messageId)
|
|
29
|
+
|
|
30
|
+
export const removePendingUserMessageId = (messageIds: string[], messageId: string) =>
|
|
31
|
+
messageIds.filter(candidateId => candidateId !== messageId)
|
|
32
|
+
|
|
33
|
+
export const reconcileOptimisticUserMessage = (
|
|
34
|
+
messages: ChatMessage[],
|
|
35
|
+
optimisticMessageId: string,
|
|
36
|
+
confirmedMessage: ChatMessage
|
|
37
|
+
) => {
|
|
38
|
+
const index = messages.findIndex(message => message.id === optimisticMessageId)
|
|
39
|
+
if (index === -1) {
|
|
40
|
+
return upsertStreamingMessage(messages, confirmedMessage)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return messages.map((message, messageIndex) =>
|
|
44
|
+
messageIndex === index ? confirmedMessage : message
|
|
45
|
+
)
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createError,
|
|
3
|
+
defineEventHandler,
|
|
4
|
+
getQuery,
|
|
5
|
+
getRouterParam
|
|
6
|
+
} from 'h3'
|
|
7
|
+
import { encodeProjectIdSegment } from '~~/shared/codori'
|
|
8
|
+
import { proxyServerFetch } from '../../../../../utils/server-proxy'
|
|
9
|
+
|
|
10
|
+
export default defineEventHandler(async (event) => {
|
|
11
|
+
const projectId = getRouterParam(event, 'projectId')
|
|
12
|
+
if (!projectId) {
|
|
13
|
+
throw createError({
|
|
14
|
+
statusCode: 400,
|
|
15
|
+
statusMessage: 'Missing project id.'
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const queryValue = getQuery(event).path
|
|
20
|
+
const path = typeof queryValue === 'string'
|
|
21
|
+
? queryValue
|
|
22
|
+
: ''
|
|
23
|
+
|
|
24
|
+
if (!path) {
|
|
25
|
+
throw createError({
|
|
26
|
+
statusCode: 400,
|
|
27
|
+
statusMessage: 'Missing attachment path.'
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const query = new URLSearchParams()
|
|
32
|
+
query.set('path', path)
|
|
33
|
+
const response = await proxyServerFetch(
|
|
34
|
+
event,
|
|
35
|
+
`/api/projects/${encodeProjectIdSegment(projectId)}/attachments/file?${query.toString()}`
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
let statusMessage = 'Attachment preview failed.'
|
|
40
|
+
try {
|
|
41
|
+
const body = await response.json() as { error?: { message?: string } }
|
|
42
|
+
statusMessage = body.error?.message ?? statusMessage
|
|
43
|
+
} catch {
|
|
44
|
+
// Ignore parse failures and fall back to the default message.
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw createError({
|
|
48
|
+
statusCode: response.status,
|
|
49
|
+
statusMessage
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const headers = new Headers()
|
|
54
|
+
for (const [key, value] of response.headers.entries()) {
|
|
55
|
+
headers.set(key, value)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return new Response(response.body, {
|
|
59
|
+
status: response.status,
|
|
60
|
+
headers
|
|
61
|
+
})
|
|
62
|
+
})
|