@codori/client 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/ChatWorkspace.vue +876 -181
- 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/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/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 +32 -10
- package/shared/codex-rpc.ts +19 -0
- package/shared/subagent-panels.ts +158 -0
|
@@ -4,6 +4,7 @@ import type { ChatMessage, ChatPart } from '~~/shared/codex-chat'
|
|
|
4
4
|
|
|
5
5
|
defineProps<{
|
|
6
6
|
message?: ChatMessage | null
|
|
7
|
+
projectId?: string
|
|
7
8
|
}>()
|
|
8
9
|
|
|
9
10
|
const partKey = (messageId: string | undefined, part: ChatPart, index: number) => {
|
|
@@ -21,6 +22,7 @@ const partKey = (messageId: string | undefined, part: ChatPart, index: number) =
|
|
|
21
22
|
v-for="(part, index) in message?.parts ?? []"
|
|
22
23
|
:key="partKey(message?.id, part, index)"
|
|
23
24
|
:message="message"
|
|
25
|
+
:project-id="projectId"
|
|
24
26
|
:part="part"
|
|
25
27
|
/>
|
|
26
28
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineComponent, h, type PropType } from 'vue'
|
|
2
2
|
import { UChatReasoning } from '#components'
|
|
3
3
|
import { EVENT_PART, ITEM_PART, type ChatMessage, type ChatPart } from '~~/shared/codex-chat'
|
|
4
|
+
import MessagePartAttachment from './message-part/Attachment.vue'
|
|
4
5
|
import MessagePartEvent from './message-part/Event.vue'
|
|
5
6
|
import MessagePartItem from './message-part/Item'
|
|
6
7
|
import MessagePartText from './message-part/Text.vue'
|
|
@@ -12,6 +13,10 @@ export default defineComponent({
|
|
|
12
13
|
type: Object as PropType<ChatMessage | null>,
|
|
13
14
|
default: null
|
|
14
15
|
},
|
|
16
|
+
projectId: {
|
|
17
|
+
type: String as PropType<string | undefined>,
|
|
18
|
+
default: undefined
|
|
19
|
+
},
|
|
15
20
|
part: {
|
|
16
21
|
type: Object as PropType<ChatPart | null>,
|
|
17
22
|
default: null
|
|
@@ -37,6 +42,11 @@ export default defineComponent({
|
|
|
37
42
|
defaultOpen: props.part.state === 'streaming',
|
|
38
43
|
autoCloseDelay: 600
|
|
39
44
|
})
|
|
45
|
+
case 'attachment':
|
|
46
|
+
return h(MessagePartAttachment, {
|
|
47
|
+
projectId: props.projectId,
|
|
48
|
+
part: props.part
|
|
49
|
+
})
|
|
40
50
|
case EVENT_PART:
|
|
41
51
|
return h(MessagePartEvent, {
|
|
42
52
|
part: props.part
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VisualSubagentPanel } from '~~/shared/codex-chat'
|
|
3
|
+
import {
|
|
4
|
+
resolveSubagentAccent,
|
|
5
|
+
resolveSubagentStatusMeta
|
|
6
|
+
} from '~~/shared/subagent-panels'
|
|
7
|
+
|
|
8
|
+
defineProps<{
|
|
9
|
+
agents: VisualSubagentPanel[]
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
expand: [threadId: string]
|
|
14
|
+
}>()
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="min-h-0">
|
|
19
|
+
<div
|
|
20
|
+
v-if="agents.length === 0"
|
|
21
|
+
class="rounded-lg border border-dashed border-default px-4 py-6 text-sm text-muted"
|
|
22
|
+
>
|
|
23
|
+
No subagents yet.
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<ul
|
|
27
|
+
v-else
|
|
28
|
+
class="divide-y divide-default"
|
|
29
|
+
>
|
|
30
|
+
<li
|
|
31
|
+
v-for="(agent, index) in agents"
|
|
32
|
+
:key="agent.threadId"
|
|
33
|
+
class="flex items-center gap-3 px-4 py-3"
|
|
34
|
+
>
|
|
35
|
+
<div
|
|
36
|
+
class="size-2.5 shrink-0 rounded-full ring-4"
|
|
37
|
+
:class="resolveSubagentAccent(index).dotClass"
|
|
38
|
+
/>
|
|
39
|
+
|
|
40
|
+
<div class="min-w-0 flex-1">
|
|
41
|
+
<p
|
|
42
|
+
class="truncate text-sm font-medium"
|
|
43
|
+
:class="resolveSubagentAccent(index).textClass"
|
|
44
|
+
>
|
|
45
|
+
{{ agent.name }}
|
|
46
|
+
</p>
|
|
47
|
+
<p class="text-xs text-muted">
|
|
48
|
+
{{ resolveSubagentStatusMeta(agent.status).label }}
|
|
49
|
+
</p>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<UButton
|
|
53
|
+
icon="i-lucide-expand"
|
|
54
|
+
color="neutral"
|
|
55
|
+
variant="ghost"
|
|
56
|
+
size="sm"
|
|
57
|
+
square
|
|
58
|
+
:aria-label="`Expand ${agent.name}`"
|
|
59
|
+
@click="emit('expand', agent.threadId)"
|
|
60
|
+
/>
|
|
61
|
+
</li>
|
|
62
|
+
</ul>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
const sharedScrollPositions = new Map<string, number>()
|
|
3
|
+
const sharedStickToBottomStates = new Map<string, boolean>()
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<script setup lang="ts">
|
|
7
|
+
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
|
8
|
+
import type { VisualSubagentPanel } from '~~/shared/codex-chat'
|
|
9
|
+
import { resolveSubagentStatusMeta, type SubagentAccent } from '~~/shared/subagent-panels'
|
|
10
|
+
|
|
11
|
+
const props = withDefaults(defineProps<{
|
|
12
|
+
agent: VisualSubagentPanel
|
|
13
|
+
accent?: SubagentAccent | null
|
|
14
|
+
expanded?: boolean
|
|
15
|
+
showExpandButton?: boolean
|
|
16
|
+
showCollapseButton?: boolean
|
|
17
|
+
scrollScope?: string
|
|
18
|
+
}>(), {
|
|
19
|
+
accent: null,
|
|
20
|
+
expanded: false,
|
|
21
|
+
showExpandButton: false,
|
|
22
|
+
showCollapseButton: false,
|
|
23
|
+
scrollScope: 'default'
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits<{
|
|
27
|
+
expand: []
|
|
28
|
+
collapse: []
|
|
29
|
+
}>()
|
|
30
|
+
|
|
31
|
+
const SCROLL_RETRY_DELAY_MS = 48
|
|
32
|
+
const SCROLL_RETRY_COUNT = 4
|
|
33
|
+
const SCROLL_STICKY_THRESHOLD_PX = 24
|
|
34
|
+
|
|
35
|
+
const scrollContainer = ref<HTMLElement | null>(null)
|
|
36
|
+
let scrollRetryTimer: number | null = null
|
|
37
|
+
let isTickPending = false
|
|
38
|
+
|
|
39
|
+
const statusMeta = computed(() => resolveSubagentStatusMeta(props.agent.status))
|
|
40
|
+
const scrollStateKey = computed(() => `${props.scrollScope}:${props.agent.threadId}`)
|
|
41
|
+
const lastMessageSignature = computed(() => {
|
|
42
|
+
const lastMessage = props.agent.messages.at(-1)
|
|
43
|
+
if (!lastMessage) {
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return JSON.stringify({
|
|
48
|
+
id: lastMessage.id,
|
|
49
|
+
pending: lastMessage.pending ?? false,
|
|
50
|
+
parts: lastMessage.parts.map((part) => {
|
|
51
|
+
if (part.type === 'text') {
|
|
52
|
+
return [part.type, part.state ?? '', part.text.length]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (part.type === 'reasoning') {
|
|
56
|
+
return [
|
|
57
|
+
part.type,
|
|
58
|
+
part.state ?? '',
|
|
59
|
+
part.summary.join('\n').length,
|
|
60
|
+
part.content.join('\n').length
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (part.type === 'attachment') {
|
|
65
|
+
return [part.type, part.attachment.name, part.attachment.url ?? part.attachment.localPath ?? '']
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return [part.type, JSON.stringify(part.data).length]
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
const panelSignature = computed(() => [
|
|
73
|
+
props.agent.messages.length,
|
|
74
|
+
lastMessageSignature.value,
|
|
75
|
+
props.agent.status ?? ''
|
|
76
|
+
].join(':'))
|
|
77
|
+
|
|
78
|
+
const clearScrollRetry = () => {
|
|
79
|
+
if (scrollRetryTimer !== null) {
|
|
80
|
+
window.clearTimeout(scrollRetryTimer)
|
|
81
|
+
scrollRetryTimer = null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isNearBottom = (container: HTMLElement) =>
|
|
86
|
+
container.scrollHeight - container.scrollTop - container.clientHeight <= SCROLL_STICKY_THRESHOLD_PX
|
|
87
|
+
|
|
88
|
+
const rememberScrollState = () => {
|
|
89
|
+
if (!scrollContainer.value) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
sharedScrollPositions.set(scrollStateKey.value, scrollContainer.value.scrollTop)
|
|
94
|
+
sharedStickToBottomStates.set(scrollStateKey.value, isNearBottom(scrollContainer.value))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const restoreScrollState = () => {
|
|
98
|
+
if (!scrollContainer.value) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const savedTop = sharedScrollPositions.get(scrollStateKey.value)
|
|
103
|
+
if (savedTop !== undefined) {
|
|
104
|
+
scrollContainer.value.scrollTop = savedTop
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
sharedStickToBottomStates.set(
|
|
108
|
+
scrollStateKey.value,
|
|
109
|
+
savedTop === undefined || isNearBottom(scrollContainer.value)
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const scrollToBottom = () => {
|
|
114
|
+
if (!scrollContainer.value) {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight
|
|
119
|
+
rememberScrollState()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const queueScrollToBottom = (attempt = 0) => {
|
|
123
|
+
if (!import.meta.client) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (attempt === 0) {
|
|
128
|
+
clearScrollRetry()
|
|
129
|
+
if (isTickPending) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
isTickPending = true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
void nextTick(() => {
|
|
136
|
+
if (attempt === 0) {
|
|
137
|
+
isTickPending = false
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
scrollToBottom()
|
|
141
|
+
if (attempt >= SCROLL_RETRY_COUNT) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
scrollRetryTimer = window.setTimeout(() => {
|
|
146
|
+
queueScrollToBottom(attempt + 1)
|
|
147
|
+
}, SCROLL_RETRY_DELAY_MS)
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const onScroll = () => {
|
|
152
|
+
rememberScrollState()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
watch(scrollContainer, (container) => {
|
|
156
|
+
if (!container) {
|
|
157
|
+
clearScrollRetry()
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
restoreScrollState()
|
|
162
|
+
if (!sharedScrollPositions.has(scrollStateKey.value)) {
|
|
163
|
+
queueScrollToBottom()
|
|
164
|
+
}
|
|
165
|
+
}, { immediate: true })
|
|
166
|
+
|
|
167
|
+
watch(() => props.agent.threadId, (_, previousThreadId) => {
|
|
168
|
+
if (previousThreadId) {
|
|
169
|
+
rememberScrollState()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
clearScrollRetry()
|
|
173
|
+
void nextTick(() => {
|
|
174
|
+
restoreScrollState()
|
|
175
|
+
if (!sharedScrollPositions.has(scrollStateKey.value)) {
|
|
176
|
+
queueScrollToBottom()
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
watch(panelSignature, () => {
|
|
182
|
+
if (sharedStickToBottomStates.get(scrollStateKey.value) === false) {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
queueScrollToBottom()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
onBeforeUnmount(() => {
|
|
190
|
+
rememberScrollState()
|
|
191
|
+
clearScrollRetry()
|
|
192
|
+
})
|
|
193
|
+
</script>
|
|
194
|
+
|
|
195
|
+
<template>
|
|
196
|
+
<section class="flex h-full min-h-0 flex-col bg-elevated/25">
|
|
197
|
+
<header
|
|
198
|
+
class="flex items-center justify-between gap-2 border-b border-default px-3 py-2"
|
|
199
|
+
:class="props.accent?.headerClass"
|
|
200
|
+
>
|
|
201
|
+
<div class="min-w-0">
|
|
202
|
+
<p
|
|
203
|
+
class="truncate text-sm font-semibold"
|
|
204
|
+
:class="props.accent?.textClass || 'text-highlighted'"
|
|
205
|
+
>
|
|
206
|
+
{{ agent.name }}
|
|
207
|
+
</p>
|
|
208
|
+
<p
|
|
209
|
+
v-if="expanded"
|
|
210
|
+
class="text-xs text-muted"
|
|
211
|
+
>
|
|
212
|
+
Focused subagent transcript
|
|
213
|
+
</p>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<div class="flex shrink-0 items-center gap-1.5">
|
|
217
|
+
<UBadge
|
|
218
|
+
:color="statusMeta.color"
|
|
219
|
+
variant="soft"
|
|
220
|
+
size="sm"
|
|
221
|
+
>
|
|
222
|
+
{{ statusMeta.label }}
|
|
223
|
+
</UBadge>
|
|
224
|
+
<UButton
|
|
225
|
+
v-if="showExpandButton"
|
|
226
|
+
icon="i-lucide-expand"
|
|
227
|
+
color="neutral"
|
|
228
|
+
variant="ghost"
|
|
229
|
+
size="xs"
|
|
230
|
+
square
|
|
231
|
+
aria-label="Expand subagent"
|
|
232
|
+
@click="emit('expand')"
|
|
233
|
+
/>
|
|
234
|
+
<UButton
|
|
235
|
+
v-if="showCollapseButton"
|
|
236
|
+
icon="i-lucide-shrink"
|
|
237
|
+
color="neutral"
|
|
238
|
+
variant="ghost"
|
|
239
|
+
size="xs"
|
|
240
|
+
square
|
|
241
|
+
aria-label="Collapse subagent"
|
|
242
|
+
@click="emit('collapse')"
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
</header>
|
|
246
|
+
|
|
247
|
+
<div
|
|
248
|
+
ref="scrollContainer"
|
|
249
|
+
class="min-h-0 flex-1 overflow-y-auto px-3 py-2"
|
|
250
|
+
@scroll="onScroll"
|
|
251
|
+
>
|
|
252
|
+
<div
|
|
253
|
+
v-if="agent.messages.length === 0"
|
|
254
|
+
class="rounded-lg border border-dashed border-default px-3 py-4 text-sm text-muted"
|
|
255
|
+
>
|
|
256
|
+
Waiting for subagent output...
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<UChatMessages
|
|
260
|
+
v-else
|
|
261
|
+
:messages="agent.messages"
|
|
262
|
+
:status="agent.status === 'pendingInit' || agent.status === 'running' ? 'streaming' : 'ready'"
|
|
263
|
+
:user="{
|
|
264
|
+
ui: {
|
|
265
|
+
root: 'scroll-mt-4',
|
|
266
|
+
container: 'gap-3 pb-8',
|
|
267
|
+
content: 'px-4 py-3 rounded-lg min-h-12'
|
|
268
|
+
}
|
|
269
|
+
}"
|
|
270
|
+
:ui="{ root: 'subagent-chat-messages min-h-full min-w-0 [&>article]:min-w-0 [&_[data-slot=content]]:min-w-0' }"
|
|
271
|
+
compact
|
|
272
|
+
>
|
|
273
|
+
<template #content="{ message }">
|
|
274
|
+
<MessageContent :message="message as VisualSubagentPanel['messages'][number]" />
|
|
275
|
+
</template>
|
|
276
|
+
</UChatMessages>
|
|
277
|
+
</div>
|
|
278
|
+
</section>
|
|
279
|
+
</template>
|
|
280
|
+
|
|
281
|
+
<style scoped>
|
|
282
|
+
.subagent-chat-messages :deep(.cd-markdown),
|
|
283
|
+
.subagent-chat-messages :deep([data-slot='content']) {
|
|
284
|
+
min-width: 0;
|
|
285
|
+
max-width: 100%;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.subagent-chat-messages :deep(.cd-markdown p),
|
|
289
|
+
.subagent-chat-messages :deep(.cd-markdown li),
|
|
290
|
+
.subagent-chat-messages :deep(.cd-markdown a),
|
|
291
|
+
.subagent-chat-messages :deep(.cd-markdown code) {
|
|
292
|
+
overflow-wrap: anywhere;
|
|
293
|
+
word-break: break-word;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.subagent-chat-messages :deep(.cd-markdown pre),
|
|
297
|
+
.subagent-chat-messages :deep(.cd-markdown .shiki),
|
|
298
|
+
.subagent-chat-messages :deep(.cd-markdown .shiki code) {
|
|
299
|
+
max-width: 100%;
|
|
300
|
+
white-space: pre-wrap !important;
|
|
301
|
+
overflow-wrap: anywhere;
|
|
302
|
+
word-break: break-word;
|
|
303
|
+
overflow-x: hidden !important;
|
|
304
|
+
}
|
|
305
|
+
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref } from 'vue'
|
|
2
|
+
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
3
3
|
import { useThreadPanel } from '../composables/useThreadPanel'
|
|
4
4
|
import ThreadList from './ThreadList.vue'
|
|
5
5
|
|
|
@@ -7,62 +7,81 @@ defineProps<{
|
|
|
7
7
|
projectId: string | null
|
|
8
8
|
}>()
|
|
9
9
|
|
|
10
|
+
const readDesktopViewport = () =>
|
|
11
|
+
import.meta.client ? window.matchMedia('(min-width: 1280px)').matches : false
|
|
12
|
+
|
|
10
13
|
const { open, closePanel } = useThreadPanel()
|
|
11
14
|
const threadListKey = ref(0)
|
|
15
|
+
const isDesktopViewport = ref(readDesktopViewport())
|
|
16
|
+
|
|
17
|
+
let viewportQuery: MediaQueryList | null = null
|
|
18
|
+
let removeViewportListener: (() => void) | null = null
|
|
12
19
|
|
|
13
20
|
const refreshThreads = () => {
|
|
14
21
|
threadListKey.value += 1
|
|
15
22
|
}
|
|
23
|
+
|
|
24
|
+
onMounted(() => {
|
|
25
|
+
if (!import.meta.client) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
viewportQuery = window.matchMedia('(min-width: 1280px)')
|
|
30
|
+
isDesktopViewport.value = viewportQuery.matches
|
|
31
|
+
|
|
32
|
+
const handleViewportChange = (event: MediaQueryListEvent) => {
|
|
33
|
+
isDesktopViewport.value = event.matches
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
viewportQuery.addEventListener('change', handleViewportChange)
|
|
37
|
+
removeViewportListener = () => {
|
|
38
|
+
viewportQuery?.removeEventListener('change', handleViewportChange)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
onBeforeUnmount(() => {
|
|
43
|
+
removeViewportListener?.()
|
|
44
|
+
})
|
|
16
45
|
</script>
|
|
17
46
|
|
|
18
47
|
<template>
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class="
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/>
|
|
44
|
-
<UButton
|
|
45
|
-
icon="i-lucide-panel-right-close"
|
|
46
|
-
color="neutral"
|
|
47
|
-
variant="ghost"
|
|
48
|
-
size="xs"
|
|
49
|
-
square
|
|
50
|
-
@click="closePanel"
|
|
51
|
-
/>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
</template>
|
|
55
|
-
<template #body>
|
|
56
|
-
<ThreadList
|
|
57
|
-
:key="threadListKey"
|
|
58
|
-
:project-id="projectId"
|
|
48
|
+
<aside
|
|
49
|
+
v-if="projectId && open && isDesktopViewport"
|
|
50
|
+
class="hidden h-full min-h-0 w-[22rem] shrink-0 flex-col border-l border-default bg-default xl:flex"
|
|
51
|
+
>
|
|
52
|
+
<div class="flex items-center justify-between gap-2 border-b border-default px-3 py-2">
|
|
53
|
+
<div class="text-sm font-medium text-highlighted">
|
|
54
|
+
Resume Threads
|
|
55
|
+
</div>
|
|
56
|
+
<div class="flex items-center gap-1">
|
|
57
|
+
<UButton
|
|
58
|
+
icon="i-lucide-refresh-cw"
|
|
59
|
+
color="neutral"
|
|
60
|
+
variant="ghost"
|
|
61
|
+
size="xs"
|
|
62
|
+
square
|
|
63
|
+
@click="refreshThreads"
|
|
64
|
+
/>
|
|
65
|
+
<UButton
|
|
66
|
+
icon="i-lucide-panel-right-close"
|
|
67
|
+
color="neutral"
|
|
68
|
+
variant="ghost"
|
|
69
|
+
size="xs"
|
|
70
|
+
square
|
|
71
|
+
@click="closePanel"
|
|
59
72
|
/>
|
|
60
|
-
</
|
|
61
|
-
</
|
|
62
|
-
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="min-h-0 flex-1 overflow-hidden">
|
|
76
|
+
<ThreadList
|
|
77
|
+
:key="threadListKey"
|
|
78
|
+
:project-id="projectId"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</aside>
|
|
63
82
|
|
|
64
83
|
<USlideover
|
|
65
|
-
v-if="projectId"
|
|
84
|
+
v-if="projectId && !isDesktopViewport"
|
|
66
85
|
v-model:open="open"
|
|
67
86
|
title="Resume Threads"
|
|
68
87
|
side="right"
|