@codori/client 0.0.8 → 0.0.9
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 +481 -64
- package/app/components/LocalFileViewerModal.vue +314 -0
- package/app/components/MessagePartRenderer.ts +1 -0
- package/app/components/SubagentTranscriptPanel.vue +5 -1
- package/app/components/UsageStatusModal.vue +265 -0
- package/app/components/VisualSubagentStack.vue +2 -0
- package/app/components/message-part/LocalFileLink.vue +51 -0
- package/app/components/message-part/Text.vue +31 -8
- package/app/composables/useLocalFileViewer.ts +46 -0
- package/app/pages/projects/[...projectId]/threads/[threadId].vue +2 -0
- package/app/utils/slash-prompt-focus.ts +8 -0
- package/package.json +1 -1
- package/server/api/codori/projects/[projectId]/local-file.get.ts +44 -0
- package/shared/account-rate-limits.ts +190 -0
- package/shared/file-autocomplete.ts +166 -0
- package/shared/file-highlighting.ts +127 -0
- package/shared/local-files.ts +122 -0
- package/shared/slash-commands.ts +13 -1
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Comark } from '@comark/vue'
|
|
3
|
+
import highlight from '@comark/vue/plugins/highlight'
|
|
4
|
+
import { computed, nextTick, ref, watch } from 'vue'
|
|
5
|
+
import { useRuntimeConfig } from '#imports'
|
|
6
|
+
import { useLocalFileViewer } from '../composables/useLocalFileViewer'
|
|
7
|
+
import { formatLocalFileSize, resolveProjectLocalFileUrl, type ProjectLocalFileResponse } from '../../shared/local-files'
|
|
8
|
+
import {
|
|
9
|
+
buildHighlightedFileMarkdown,
|
|
10
|
+
inferLocalFileLanguage,
|
|
11
|
+
resolveLocalFileLanguageLabel
|
|
12
|
+
} from '../../shared/file-highlighting'
|
|
13
|
+
|
|
14
|
+
const runtimeConfig = useRuntimeConfig()
|
|
15
|
+
const { state, closeViewer } = useLocalFileViewer()
|
|
16
|
+
|
|
17
|
+
const loading = ref(false)
|
|
18
|
+
const error = ref<string | null>(null)
|
|
19
|
+
const file = ref<ProjectLocalFileResponse['file'] | null>(null)
|
|
20
|
+
const lineContainer = ref<HTMLElement | null>(null)
|
|
21
|
+
const viewerPlugins = [
|
|
22
|
+
highlight({ preStyles: false })
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const isOpen = computed({
|
|
26
|
+
get: () => state.value.open,
|
|
27
|
+
set: (open: boolean) => {
|
|
28
|
+
if (!open) {
|
|
29
|
+
closeViewer()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const relativePathLabel = computed(() => {
|
|
35
|
+
if (!file.value) {
|
|
36
|
+
return state.value.path ?? ''
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return file.value.relativePath || file.value.name
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const lineCount = computed(() => {
|
|
43
|
+
if (!file.value) {
|
|
44
|
+
return 0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return file.value.text.split('\n').length
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const inferredLanguage = computed(() =>
|
|
51
|
+
file.value ? inferLocalFileLanguage(file.value.path, file.value.text) : null
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const languageLabel = computed(() =>
|
|
55
|
+
resolveLocalFileLanguageLabel(inferredLanguage.value)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const highlightedMarkdown = computed(() => {
|
|
59
|
+
if (!file.value) {
|
|
60
|
+
return ''
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return buildHighlightedFileMarkdown(file.value.text, inferredLanguage.value)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const lineNumberWidth = computed(() =>
|
|
67
|
+
`${Math.max(3, String(lineCount.value || 1).length + 1)}ch`
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const updatedAtLabel = computed(() => {
|
|
71
|
+
if (!file.value) {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
76
|
+
month: 'short',
|
|
77
|
+
day: 'numeric',
|
|
78
|
+
hour: 'numeric',
|
|
79
|
+
minute: '2-digit'
|
|
80
|
+
}).format(new Date(file.value.updatedAt))
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const syncRenderedCodeLines = async () => {
|
|
84
|
+
if (!lineContainer.value) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await nextTick()
|
|
89
|
+
requestAnimationFrame(() => {
|
|
90
|
+
const renderedLines = Array.from(lineContainer.value?.querySelectorAll<HTMLElement>('.line') ?? [])
|
|
91
|
+
for (const [index, line] of renderedLines.entries()) {
|
|
92
|
+
const lineNumber = index + 1
|
|
93
|
+
line.dataset.fileLine = String(lineNumber)
|
|
94
|
+
line.classList.toggle('is-target-line', state.value.line === lineNumber)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const targetLine = state.value.line
|
|
98
|
+
if (!targetLine) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const target = lineContainer.value?.querySelector<HTMLElement>(`[data-file-line="${targetLine}"]`)
|
|
103
|
+
target?.scrollIntoView({
|
|
104
|
+
block: 'center'
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
watch(
|
|
110
|
+
() => [state.value.open, state.value.projectId, state.value.path] as const,
|
|
111
|
+
async ([open, projectId, path]) => {
|
|
112
|
+
if (!open || !projectId || !path) {
|
|
113
|
+
loading.value = false
|
|
114
|
+
error.value = null
|
|
115
|
+
file.value = null
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
loading.value = true
|
|
120
|
+
error.value = null
|
|
121
|
+
file.value = null
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const response = await $fetch<ProjectLocalFileResponse>(resolveProjectLocalFileUrl({
|
|
125
|
+
projectId,
|
|
126
|
+
path,
|
|
127
|
+
configuredBase: String(runtimeConfig.public.serverBase ?? '')
|
|
128
|
+
}))
|
|
129
|
+
if (!state.value.open || state.value.projectId !== projectId || state.value.path !== path) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
file.value = response.file
|
|
134
|
+
await syncRenderedCodeLines()
|
|
135
|
+
} catch (caughtError) {
|
|
136
|
+
if (!state.value.open || state.value.projectId !== projectId || state.value.path !== path) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
error.value = caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
141
|
+
} finally {
|
|
142
|
+
if (state.value.open && state.value.projectId === projectId && state.value.path === path) {
|
|
143
|
+
loading.value = false
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
{ immediate: true }
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
watch(() => state.value.line, () => {
|
|
151
|
+
if (file.value) {
|
|
152
|
+
void syncRenderedCodeLines()
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
watch(highlightedMarkdown, () => {
|
|
157
|
+
if (file.value) {
|
|
158
|
+
void syncRenderedCodeLines()
|
|
159
|
+
}
|
|
160
|
+
}, { flush: 'post' })
|
|
161
|
+
</script>
|
|
162
|
+
|
|
163
|
+
<template>
|
|
164
|
+
<UModal
|
|
165
|
+
v-model:open="isOpen"
|
|
166
|
+
fullscreen
|
|
167
|
+
:ui="{
|
|
168
|
+
header: 'hidden',
|
|
169
|
+
close: 'hidden',
|
|
170
|
+
content: 'overflow-hidden bg-default',
|
|
171
|
+
body: '!h-full !p-0'
|
|
172
|
+
}"
|
|
173
|
+
>
|
|
174
|
+
<template #body>
|
|
175
|
+
<section class="flex h-full min-h-0 flex-col bg-default">
|
|
176
|
+
<header class="flex items-center justify-between gap-3 border-b border-default px-4 py-3">
|
|
177
|
+
<div class="min-w-0">
|
|
178
|
+
<div class="truncate text-xs font-medium uppercase tracking-[0.22em] text-primary">
|
|
179
|
+
Local File Viewer
|
|
180
|
+
</div>
|
|
181
|
+
<div class="truncate text-sm font-semibold text-highlighted">
|
|
182
|
+
{{ relativePathLabel }}
|
|
183
|
+
</div>
|
|
184
|
+
<div class="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted">
|
|
185
|
+
<span v-if="file">{{ formatLocalFileSize(file.size) }}</span>
|
|
186
|
+
<span v-if="file">{{ lineCount }} lines</span>
|
|
187
|
+
<span v-if="updatedAtLabel">{{ updatedAtLabel }}</span>
|
|
188
|
+
<span v-if="languageLabel">{{ languageLabel }}</span>
|
|
189
|
+
<span v-if="state.line">Line {{ state.line }}</span>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<UButton
|
|
194
|
+
icon="i-lucide-x"
|
|
195
|
+
color="neutral"
|
|
196
|
+
variant="ghost"
|
|
197
|
+
size="sm"
|
|
198
|
+
aria-label="Close local file viewer"
|
|
199
|
+
@click="closeViewer"
|
|
200
|
+
/>
|
|
201
|
+
</header>
|
|
202
|
+
|
|
203
|
+
<div
|
|
204
|
+
v-if="loading"
|
|
205
|
+
class="flex min-h-0 flex-1 items-center justify-center px-6 py-10 text-sm text-muted"
|
|
206
|
+
>
|
|
207
|
+
Loading local file preview...
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div
|
|
211
|
+
v-else-if="error"
|
|
212
|
+
class="flex min-h-0 flex-1 items-center justify-center px-6 py-10"
|
|
213
|
+
>
|
|
214
|
+
<UAlert
|
|
215
|
+
color="error"
|
|
216
|
+
variant="soft"
|
|
217
|
+
icon="i-lucide-circle-alert"
|
|
218
|
+
:title="error"
|
|
219
|
+
class="w-full max-w-2xl"
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<div
|
|
224
|
+
v-else-if="file"
|
|
225
|
+
ref="lineContainer"
|
|
226
|
+
class="local-file-viewer-code min-h-0 flex-1 overflow-auto bg-elevated/15"
|
|
227
|
+
:style="{ '--lfv-line-number-width': lineNumberWidth }"
|
|
228
|
+
>
|
|
229
|
+
<Suspense>
|
|
230
|
+
<Comark
|
|
231
|
+
class="local-file-viewer-markdown"
|
|
232
|
+
:markdown="highlightedMarkdown"
|
|
233
|
+
:plugins="viewerPlugins"
|
|
234
|
+
/>
|
|
235
|
+
</Suspense>
|
|
236
|
+
</div>
|
|
237
|
+
</section>
|
|
238
|
+
</template>
|
|
239
|
+
</UModal>
|
|
240
|
+
</template>
|
|
241
|
+
|
|
242
|
+
<style scoped>
|
|
243
|
+
.local-file-viewer-code :deep(.local-file-viewer-markdown) {
|
|
244
|
+
min-width: max-content;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.local-file-viewer-code :deep(.local-file-viewer-markdown > * + *) {
|
|
248
|
+
margin-top: 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.local-file-viewer-code :deep(pre),
|
|
252
|
+
.local-file-viewer-code :deep(.shiki) {
|
|
253
|
+
margin: 0;
|
|
254
|
+
min-width: max-content;
|
|
255
|
+
border: 0;
|
|
256
|
+
border-radius: 0;
|
|
257
|
+
padding: 0;
|
|
258
|
+
background: transparent !important;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.local-file-viewer-code :deep(pre code),
|
|
262
|
+
.local-file-viewer-code :deep(.shiki code) {
|
|
263
|
+
display: block;
|
|
264
|
+
min-width: max-content;
|
|
265
|
+
padding: 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.local-file-viewer-code :deep(.line) {
|
|
269
|
+
display: block;
|
|
270
|
+
min-width: max-content;
|
|
271
|
+
padding: 0 1.5rem 0 0;
|
|
272
|
+
padding-left: calc(var(--lfv-line-number-width) + 1.5rem);
|
|
273
|
+
position: relative;
|
|
274
|
+
white-space: pre;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.local-file-viewer-code :deep(.line::before) {
|
|
278
|
+
content: attr(data-file-line);
|
|
279
|
+
position: absolute;
|
|
280
|
+
top: 0;
|
|
281
|
+
left: 0;
|
|
282
|
+
width: var(--lfv-line-number-width);
|
|
283
|
+
padding-right: 0.75rem;
|
|
284
|
+
text-align: right;
|
|
285
|
+
color: var(--ui-text-muted);
|
|
286
|
+
user-select: none;
|
|
287
|
+
font-variant-numeric: tabular-nums;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.local-file-viewer-code :deep(.line.is-target-line) {
|
|
291
|
+
background: color-mix(in srgb, var(--ui-primary) 10%, transparent);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.local-file-viewer-code :deep(.line.is-target-line::before) {
|
|
295
|
+
color: var(--ui-primary);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.local-file-viewer-code :deep(.shiki),
|
|
299
|
+
.local-file-viewer-code :deep(.shiki code),
|
|
300
|
+
.local-file-viewer-code :deep(pre),
|
|
301
|
+
.local-file-viewer-code :deep(pre code) {
|
|
302
|
+
font-size: 13px;
|
|
303
|
+
line-height: 1.75;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
:global(.dark) .local-file-viewer-code :deep(.shiki),
|
|
307
|
+
:global(.dark) .local-file-viewer-code :deep(.shiki span) {
|
|
308
|
+
color: var(--shiki-dark) !important;
|
|
309
|
+
background-color: var(--shiki-dark-bg) !important;
|
|
310
|
+
font-style: var(--shiki-dark-font-style) !important;
|
|
311
|
+
font-weight: var(--shiki-dark-font-weight) !important;
|
|
312
|
+
text-decoration: var(--shiki-dark-text-decoration) !important;
|
|
313
|
+
}
|
|
314
|
+
</style>
|
|
@@ -10,6 +10,7 @@ import { resolveSubagentStatusMeta, type SubagentAccent } from '~~/shared/subage
|
|
|
10
10
|
|
|
11
11
|
const props = withDefaults(defineProps<{
|
|
12
12
|
agent: VisualSubagentPanel
|
|
13
|
+
projectId?: string | null
|
|
13
14
|
accent?: SubagentAccent | null
|
|
14
15
|
expanded?: boolean
|
|
15
16
|
showExpandButton?: boolean
|
|
@@ -271,7 +272,10 @@ onBeforeUnmount(() => {
|
|
|
271
272
|
compact
|
|
272
273
|
>
|
|
273
274
|
<template #content="{ message }">
|
|
274
|
-
<MessageContent
|
|
275
|
+
<MessageContent
|
|
276
|
+
:message="message as VisualSubagentPanel['messages'][number]"
|
|
277
|
+
:project-id="projectId ?? undefined"
|
|
278
|
+
/>
|
|
275
279
|
</template>
|
|
276
280
|
</UChatMessages>
|
|
277
281
|
</div>
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
|
3
|
+
import { useProjects } from '../composables/useProjects'
|
|
4
|
+
import { useRpc } from '../composables/useRpc'
|
|
5
|
+
import {
|
|
6
|
+
normalizeAccountRateLimits,
|
|
7
|
+
formatRateLimitWindowDuration,
|
|
8
|
+
type RateLimitBucket
|
|
9
|
+
} from '../../shared/account-rate-limits'
|
|
10
|
+
|
|
11
|
+
const props = withDefaults(defineProps<{
|
|
12
|
+
projectId: string
|
|
13
|
+
open?: boolean
|
|
14
|
+
}>(), {
|
|
15
|
+
open: false
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
'update:open': [open: boolean]
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const { getClient } = useRpc()
|
|
23
|
+
const {
|
|
24
|
+
loaded,
|
|
25
|
+
refreshProjects,
|
|
26
|
+
getProject,
|
|
27
|
+
startProject
|
|
28
|
+
} = useProjects()
|
|
29
|
+
const loading = ref(false)
|
|
30
|
+
const error = ref<string | null>(null)
|
|
31
|
+
const buckets = ref<RateLimitBucket[]>([])
|
|
32
|
+
const selectedProject = computed(() => getProject(props.projectId))
|
|
33
|
+
let usageStatusLoadToken = 0
|
|
34
|
+
let releaseUsageStatusSubscription: (() => void) | null = null
|
|
35
|
+
|
|
36
|
+
const nextResetFormatter = new Intl.DateTimeFormat(undefined, {
|
|
37
|
+
month: 'short',
|
|
38
|
+
day: 'numeric',
|
|
39
|
+
hour: 'numeric',
|
|
40
|
+
minute: '2-digit'
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const toRemainingPercent = (usedPercent: number | null) =>
|
|
44
|
+
usedPercent == null ? null : Math.max(0, 100 - usedPercent)
|
|
45
|
+
|
|
46
|
+
const formatProgressLabel = (usedPercent: number | null) => {
|
|
47
|
+
const remainingPercent = toRemainingPercent(usedPercent)
|
|
48
|
+
return remainingPercent == null ? 'Remaining quota unavailable' : `${Math.round(remainingPercent)}% remaining`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const formatNextResetLabel = (value: string | null) =>
|
|
52
|
+
value ? `Next reset ${nextResetFormatter.format(new Date(value))}` : 'Next reset unavailable'
|
|
53
|
+
|
|
54
|
+
const cards = computed(() =>
|
|
55
|
+
buckets.value.map((bucket) => {
|
|
56
|
+
const windows = [{
|
|
57
|
+
id: `${bucket.limitId}-primary`,
|
|
58
|
+
title: formatRateLimitWindowDuration(bucket.primary?.windowDurationMins ?? null) ?? 'Primary window',
|
|
59
|
+
remainingPercent: toRemainingPercent(bucket.primary?.usedPercent ?? null),
|
|
60
|
+
progressLabel: formatProgressLabel(bucket.primary?.usedPercent ?? null),
|
|
61
|
+
resetsAt: bucket.primary?.resetsAt ?? null,
|
|
62
|
+
nextResetLabel: formatNextResetLabel(bucket.primary?.resetsAt ?? null),
|
|
63
|
+
durationMins: bucket.primary?.windowDurationMins ?? null
|
|
64
|
+
}, {
|
|
65
|
+
id: `${bucket.limitId}-secondary`,
|
|
66
|
+
title: formatRateLimitWindowDuration(bucket.secondary?.windowDurationMins ?? null) ?? 'Secondary window',
|
|
67
|
+
remainingPercent: toRemainingPercent(bucket.secondary?.usedPercent ?? null),
|
|
68
|
+
progressLabel: formatProgressLabel(bucket.secondary?.usedPercent ?? null),
|
|
69
|
+
resetsAt: bucket.secondary?.resetsAt ?? null,
|
|
70
|
+
nextResetLabel: formatNextResetLabel(bucket.secondary?.resetsAt ?? null),
|
|
71
|
+
durationMins: bucket.secondary?.windowDurationMins ?? null
|
|
72
|
+
}]
|
|
73
|
+
.filter((window) =>
|
|
74
|
+
window.remainingPercent != null
|
|
75
|
+
|| window.resetsAt != null
|
|
76
|
+
|| window.durationMins != null
|
|
77
|
+
)
|
|
78
|
+
.sort((left, right) => (left.durationMins ?? Number.POSITIVE_INFINITY) - (right.durationMins ?? Number.POSITIVE_INFINITY))
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
id: bucket.limitId,
|
|
82
|
+
label: bucket.limitName ?? bucket.limitId,
|
|
83
|
+
detail: bucket.limitName && bucket.limitName !== bucket.limitId ? bucket.limitId : null,
|
|
84
|
+
windows
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const resetUsageStatusState = () => {
|
|
90
|
+
loading.value = false
|
|
91
|
+
error.value = null
|
|
92
|
+
buckets.value = []
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const closeUsageStatus = () => {
|
|
96
|
+
usageStatusLoadToken += 1
|
|
97
|
+
releaseUsageStatusSubscription?.()
|
|
98
|
+
releaseUsageStatusSubscription = null
|
|
99
|
+
resetUsageStatusState()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const applyUsageStatusSnapshot = (value: unknown) => {
|
|
103
|
+
buckets.value = normalizeAccountRateLimits(value)
|
|
104
|
+
error.value = null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const ensureProjectRuntime = async () => {
|
|
108
|
+
if (!loaded.value) {
|
|
109
|
+
await refreshProjects()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (selectedProject.value?.status === 'running') {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await startProject(props.projectId)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const openUsageStatus = async () => {
|
|
120
|
+
usageStatusLoadToken += 1
|
|
121
|
+
const loadToken = usageStatusLoadToken
|
|
122
|
+
loading.value = true
|
|
123
|
+
error.value = null
|
|
124
|
+
buckets.value = []
|
|
125
|
+
|
|
126
|
+
const client = getClient(props.projectId)
|
|
127
|
+
releaseUsageStatusSubscription?.()
|
|
128
|
+
releaseUsageStatusSubscription = client.subscribe((notification) => {
|
|
129
|
+
if (notification.method !== 'account/rateLimits/updated' || !props.open) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
applyUsageStatusSnapshot(notification.params)
|
|
134
|
+
loading.value = false
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await ensureProjectRuntime()
|
|
139
|
+
const response = await client.request('account/rateLimits/read')
|
|
140
|
+
if (!props.open || loadToken !== usageStatusLoadToken) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
applyUsageStatusSnapshot(response)
|
|
145
|
+
} catch (caughtError) {
|
|
146
|
+
if (!props.open || loadToken !== usageStatusLoadToken) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
error.value = caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
151
|
+
} finally {
|
|
152
|
+
if (loadToken === usageStatusLoadToken) {
|
|
153
|
+
loading.value = false
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
watch(() => [props.open, props.projectId] as const, ([open, projectId], previous) => {
|
|
159
|
+
const [wasOpen, previousProjectId] = previous ?? [false, projectId]
|
|
160
|
+
|
|
161
|
+
if (!open) {
|
|
162
|
+
closeUsageStatus()
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!wasOpen || projectId !== previousProjectId) {
|
|
167
|
+
void openUsageStatus()
|
|
168
|
+
}
|
|
169
|
+
}, { immediate: true })
|
|
170
|
+
|
|
171
|
+
onBeforeUnmount(() => {
|
|
172
|
+
closeUsageStatus()
|
|
173
|
+
})
|
|
174
|
+
</script>
|
|
175
|
+
|
|
176
|
+
<template>
|
|
177
|
+
<UModal
|
|
178
|
+
:open="open"
|
|
179
|
+
title="Usage status"
|
|
180
|
+
@update:open="emit('update:open', $event)"
|
|
181
|
+
>
|
|
182
|
+
<template #body>
|
|
183
|
+
<div class="space-y-3">
|
|
184
|
+
<div
|
|
185
|
+
v-if="loading"
|
|
186
|
+
class="rounded-lg border border-default bg-elevated/40 px-4 py-6 text-sm text-muted"
|
|
187
|
+
>
|
|
188
|
+
Loading current quota windows...
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<UAlert
|
|
192
|
+
v-else-if="error"
|
|
193
|
+
color="error"
|
|
194
|
+
variant="soft"
|
|
195
|
+
icon="i-lucide-circle-alert"
|
|
196
|
+
:title="error"
|
|
197
|
+
/>
|
|
198
|
+
|
|
199
|
+
<div
|
|
200
|
+
v-else-if="!cards.length"
|
|
201
|
+
class="rounded-lg border border-dashed border-default px-4 py-6 text-center text-sm text-muted"
|
|
202
|
+
>
|
|
203
|
+
No live quota windows were reported.
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div
|
|
207
|
+
v-else
|
|
208
|
+
class="space-y-3"
|
|
209
|
+
>
|
|
210
|
+
<section
|
|
211
|
+
v-for="card in cards"
|
|
212
|
+
:key="card.id"
|
|
213
|
+
class="rounded-lg border border-default bg-elevated/35 p-3 sm:p-4"
|
|
214
|
+
>
|
|
215
|
+
<div class="mb-3 min-w-0">
|
|
216
|
+
<div class="min-w-0">
|
|
217
|
+
<h3 class="truncate text-sm font-semibold text-highlighted">
|
|
218
|
+
{{ card.label }}
|
|
219
|
+
</h3>
|
|
220
|
+
<p
|
|
221
|
+
v-if="card.detail"
|
|
222
|
+
class="text-xs text-muted"
|
|
223
|
+
>
|
|
224
|
+
{{ card.detail }}
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<div class="space-y-2">
|
|
230
|
+
<div
|
|
231
|
+
v-for="window in card.windows"
|
|
232
|
+
:key="window.id"
|
|
233
|
+
class="rounded-lg border border-default/80 bg-default/75 p-3"
|
|
234
|
+
>
|
|
235
|
+
<div class="flex items-start justify-between gap-3">
|
|
236
|
+
<div class="text-xs font-semibold uppercase tracking-[0.18em] text-muted">
|
|
237
|
+
{{ window.title }}
|
|
238
|
+
</div>
|
|
239
|
+
<div class="text-sm font-semibold text-highlighted">
|
|
240
|
+
{{ window.progressLabel }}
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<div class="mt-2 h-2 overflow-hidden rounded-full bg-muted">
|
|
245
|
+
<div
|
|
246
|
+
class="h-full rounded-full bg-primary transition-[width]"
|
|
247
|
+
:style="{ width: `${window.remainingPercent ?? 0}%` }"
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<time
|
|
252
|
+
v-if="window.resetsAt"
|
|
253
|
+
class="mt-2 block text-xs text-muted"
|
|
254
|
+
:datetime="window.resetsAt"
|
|
255
|
+
>
|
|
256
|
+
{{ window.nextResetLabel }}
|
|
257
|
+
</time>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</section>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</template>
|
|
264
|
+
</UModal>
|
|
265
|
+
</template>
|
|
@@ -5,6 +5,7 @@ import { resolveSubagentAccent } from '~~/shared/subagent-panels'
|
|
|
5
5
|
|
|
6
6
|
const props = defineProps<{
|
|
7
7
|
agents: VisualSubagentPanel[]
|
|
8
|
+
projectId?: string | null
|
|
8
9
|
}>()
|
|
9
10
|
|
|
10
11
|
const emit = defineEmits<{
|
|
@@ -26,6 +27,7 @@ const paneSize = computed(() => {
|
|
|
26
27
|
v-for="(agent, index) in agents"
|
|
27
28
|
:key="agent.threadId"
|
|
28
29
|
:agent="agent"
|
|
30
|
+
:project-id="projectId ?? null"
|
|
29
31
|
:accent="resolveSubagentAccent(index)"
|
|
30
32
|
scroll-scope="stack"
|
|
31
33
|
show-expand-button
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useProjects } from '../../composables/useProjects'
|
|
4
|
+
import { useLocalFileViewer } from '../../composables/useLocalFileViewer'
|
|
5
|
+
import { isLocalFileWithinProject, parseLocalFileHref } from '../../../shared/local-files'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
href?: string | null
|
|
9
|
+
title?: string | null
|
|
10
|
+
projectId?: string | null
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const { getProject } = useProjects()
|
|
14
|
+
const { openViewer } = useLocalFileViewer()
|
|
15
|
+
|
|
16
|
+
const parsedTarget = computed(() =>
|
|
17
|
+
props.href ? parseLocalFileHref(props.href) : null
|
|
18
|
+
)
|
|
19
|
+
const projectPath = computed(() =>
|
|
20
|
+
props.projectId ? getProject(props.projectId)?.projectPath ?? null : null
|
|
21
|
+
)
|
|
22
|
+
const isProjectLocalFile = computed(() =>
|
|
23
|
+
parsedTarget.value
|
|
24
|
+
&& isLocalFileWithinProject(parsedTarget.value.path, projectPath.value)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const onClick = (event: MouseEvent) => {
|
|
28
|
+
if (!props.projectId || !parsedTarget.value || !isProjectLocalFile.value) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
event.preventDefault()
|
|
33
|
+
openViewer({
|
|
34
|
+
projectId: props.projectId,
|
|
35
|
+
path: parsedTarget.value.path,
|
|
36
|
+
line: parsedTarget.value.line,
|
|
37
|
+
column: parsedTarget.value.column
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<a
|
|
44
|
+
:href="href ?? undefined"
|
|
45
|
+
:title="title ?? undefined"
|
|
46
|
+
class="underline decoration-primary/35 underline-offset-3 transition hover:text-primary hover:decoration-primary"
|
|
47
|
+
@click="onClick"
|
|
48
|
+
>
|
|
49
|
+
<slot />
|
|
50
|
+
</a>
|
|
51
|
+
</template>
|