arkaos 2.84.0 → 2.86.0
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/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.86.0
|
|
@@ -2,12 +2,73 @@
|
|
|
2
2
|
import type { TableColumn } from '@nuxt/ui'
|
|
3
3
|
import type { Agent } from '~/types'
|
|
4
4
|
|
|
5
|
-
const { fetchApi } = useApi()
|
|
5
|
+
const { fetchApi, apiBase } = useApi()
|
|
6
|
+
const toast = useToast()
|
|
6
7
|
|
|
7
8
|
const { data, status, error, refresh } = await fetchApi<{ agents: Agent[], total: number }>('/api/agents')
|
|
8
9
|
|
|
10
|
+
// PR69 v2.86.0 — per-department activity from PR47 telemetry.
|
|
11
|
+
// Used to badge agents whose department has run recently and to
|
|
12
|
+
// surface "no activity yet" hint when a department's never been
|
|
13
|
+
// invoked. Failure-tolerant — returns empty if telemetry unavailable.
|
|
14
|
+
interface ActivityRow {
|
|
15
|
+
call_count: number
|
|
16
|
+
total_cost_usd: number | null
|
|
17
|
+
total_tokens_in: number
|
|
18
|
+
total_tokens_out: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
data: activityData,
|
|
23
|
+
refresh: refreshActivity,
|
|
24
|
+
} = fetchApi<{ by_department: Record<string, ActivityRow>, period: string }>(
|
|
25
|
+
'/api/agents/activity?period=week',
|
|
26
|
+
)
|
|
27
|
+
|
|
9
28
|
const agents = computed(() => data.value?.agents ?? [])
|
|
10
29
|
|
|
30
|
+
function deptActivity(dept: string): ActivityRow | undefined {
|
|
31
|
+
return activityData.value?.by_department?.[dept]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const copied = ref<string | null>(null)
|
|
35
|
+
let copyTimer: ReturnType<typeof setTimeout> | null = null
|
|
36
|
+
|
|
37
|
+
async function copyAgentMention(agent: Agent) {
|
|
38
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
|
39
|
+
toast.add({ title: 'Clipboard unavailable', color: 'warning' })
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
// The most useful copy for an operator: a ready-to-paste mention
|
|
43
|
+
// that names the agent + their role so the orchestrator can dispatch.
|
|
44
|
+
const text = `Use ${agent.name} (${agent.role}, dept ${agent.department}, tier ${agent.tier}) for this task.`
|
|
45
|
+
try {
|
|
46
|
+
await navigator.clipboard.writeText(text)
|
|
47
|
+
copied.value = agent.id
|
|
48
|
+
if (copyTimer) clearTimeout(copyTimer)
|
|
49
|
+
copyTimer = setTimeout(() => { copied.value = null; copyTimer = null }, 1500)
|
|
50
|
+
toast.add({
|
|
51
|
+
title: 'Copied',
|
|
52
|
+
description: `${agent.name} mention ready to paste.`,
|
|
53
|
+
color: 'success',
|
|
54
|
+
})
|
|
55
|
+
} catch (err) {
|
|
56
|
+
toast.add({
|
|
57
|
+
title: 'Copy failed',
|
|
58
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
59
|
+
color: 'error',
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onBeforeUnmount(() => {
|
|
65
|
+
if (copyTimer) clearTimeout(copyTimer)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
async function refreshAll() {
|
|
69
|
+
await Promise.all([refresh(), refreshActivity()])
|
|
70
|
+
}
|
|
71
|
+
|
|
11
72
|
const search = ref('')
|
|
12
73
|
const departmentFilter = ref('all')
|
|
13
74
|
const tierFilter = ref('all')
|
|
@@ -77,35 +138,18 @@ const tierColor = (tier: number) => {
|
|
|
77
138
|
}
|
|
78
139
|
|
|
79
140
|
const columns: TableColumn<Agent>[] = [
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
accessorKey: 'role',
|
|
86
|
-
header: 'Role'
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
accessorKey: 'department',
|
|
90
|
-
header: 'Department'
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
accessorKey: 'tier',
|
|
94
|
-
header: 'Tier'
|
|
95
|
-
},
|
|
141
|
+
{ accessorKey: 'name', header: 'Name' },
|
|
142
|
+
{ accessorKey: 'role', header: 'Role' },
|
|
143
|
+
{ accessorKey: 'department', header: 'Department' },
|
|
144
|
+
{ accessorKey: 'tier', header: 'Tier' },
|
|
96
145
|
{
|
|
97
146
|
accessorFn: (row: Agent) => row.disc?.primary ?? '-',
|
|
98
147
|
id: 'disc',
|
|
99
|
-
header: 'DISC'
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
accessorKey: 'mbti',
|
|
103
|
-
header: 'MBTI'
|
|
148
|
+
header: 'DISC',
|
|
104
149
|
},
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
150
|
+
{ accessorKey: 'mbti', header: 'MBTI' },
|
|
151
|
+
{ id: 'activity', header: 'Activity (7d)' },
|
|
152
|
+
{ id: 'actions', header: '' },
|
|
109
153
|
]
|
|
110
154
|
|
|
111
155
|
function goToAgent(id: string) {
|
|
@@ -134,7 +178,7 @@ function goToAgent(id: string) {
|
|
|
134
178
|
empty-title="No agents found"
|
|
135
179
|
empty-icon="i-lucide-users"
|
|
136
180
|
loading-label="Loading agents"
|
|
137
|
-
:on-retry="() =>
|
|
181
|
+
:on-retry="() => refreshAll()"
|
|
138
182
|
>
|
|
139
183
|
<div class="flex flex-wrap items-center gap-3 mb-4">
|
|
140
184
|
<UInput
|
|
@@ -195,8 +239,33 @@ function goToAgent(id: string) {
|
|
|
195
239
|
<template #mbti-cell="{ row }">
|
|
196
240
|
<span class="font-mono text-sm">{{ row.original.mbti || '-' }}</span>
|
|
197
241
|
</template>
|
|
242
|
+
<template #activity-cell="{ row }">
|
|
243
|
+
<template v-if="deptActivity(row.original.department)">
|
|
244
|
+
<div class="flex items-center gap-2">
|
|
245
|
+
<span class="inline-block size-2 rounded-full bg-green-500" />
|
|
246
|
+
<span class="text-xs font-mono">
|
|
247
|
+
{{ deptActivity(row.original.department)?.call_count ?? 0 }} calls
|
|
248
|
+
</span>
|
|
249
|
+
</div>
|
|
250
|
+
</template>
|
|
251
|
+
<span v-else class="text-xs text-muted">—</span>
|
|
252
|
+
</template>
|
|
198
253
|
<template #actions-cell="{ row }">
|
|
199
|
-
<UButton
|
|
254
|
+
<UButton
|
|
255
|
+
:icon="copied === row.original.id ? 'i-lucide-check' : 'i-lucide-copy'"
|
|
256
|
+
:color="copied === row.original.id ? 'success' : 'neutral'"
|
|
257
|
+
variant="ghost"
|
|
258
|
+
size="xs"
|
|
259
|
+
aria-label="Copy agent mention"
|
|
260
|
+
@click.stop="copyAgentMention(row.original)"
|
|
261
|
+
/>
|
|
262
|
+
<UButton
|
|
263
|
+
size="xs"
|
|
264
|
+
variant="ghost"
|
|
265
|
+
icon="i-lucide-arrow-right"
|
|
266
|
+
aria-label="Open agent detail"
|
|
267
|
+
@click="goToAgent(row.original.id)"
|
|
268
|
+
/>
|
|
200
269
|
</template>
|
|
201
270
|
</UTable>
|
|
202
271
|
|
|
@@ -1,76 +1,194 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
// PR68 v2.85.0 — Commands page: ▶ Copy + ★ Favorites.
|
|
3
|
+
//
|
|
4
|
+
// The previous Commands page was a read-only catalogue (135 rows,
|
|
5
|
+
// search, filter, expand for keywords). Daniel Ek's audit question
|
|
6
|
+
// landed: "what is the job-to-be-done here vs the CLI?" The answer:
|
|
7
|
+
// fast lookup → copy to clipboard → paste back into Claude Code.
|
|
8
|
+
// PR68 makes that flow one-click + adds operator-curated favorites
|
|
9
|
+
// stored locally so the top-of-list is the operator's actual
|
|
10
|
+
// muscle-memory commands.
|
|
11
|
+
|
|
2
12
|
import type { TableColumn } from '@nuxt/ui'
|
|
3
13
|
import type { Command } from '~/types'
|
|
4
14
|
|
|
5
15
|
const { fetchApi } = useApi()
|
|
16
|
+
const toast = useToast()
|
|
6
17
|
|
|
7
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
data,
|
|
20
|
+
status,
|
|
21
|
+
error,
|
|
22
|
+
refresh,
|
|
23
|
+
} = await fetchApi<{ commands: Command[], total: number }>('/api/commands')
|
|
8
24
|
|
|
9
|
-
|
|
25
|
+
// ─── Favorites (persisted in localStorage) ───────────────────────────────
|
|
26
|
+
|
|
27
|
+
const FAVORITES_KEY = 'arkaos_command_favorites'
|
|
28
|
+
const favorites = ref<Set<string>>(new Set())
|
|
29
|
+
|
|
30
|
+
function loadFavorites() {
|
|
31
|
+
if (typeof window === 'undefined') return
|
|
32
|
+
try {
|
|
33
|
+
const raw = window.localStorage.getItem(FAVORITES_KEY)
|
|
34
|
+
if (!raw) return
|
|
35
|
+
const parsed = JSON.parse(raw)
|
|
36
|
+
if (Array.isArray(parsed)) {
|
|
37
|
+
favorites.value = new Set(parsed.filter((v): v is string => typeof v === 'string'))
|
|
38
|
+
}
|
|
39
|
+
} catch { /* corrupt JSON — ignore, start fresh */ }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function persistFavorites() {
|
|
43
|
+
if (typeof window === 'undefined') return
|
|
44
|
+
try {
|
|
45
|
+
window.localStorage.setItem(
|
|
46
|
+
FAVORITES_KEY,
|
|
47
|
+
JSON.stringify(Array.from(favorites.value)),
|
|
48
|
+
)
|
|
49
|
+
} catch { /* quota / disabled storage — silent */ }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toggleFavorite(commandId: string) {
|
|
53
|
+
if (favorites.value.has(commandId)) {
|
|
54
|
+
favorites.value.delete(commandId)
|
|
55
|
+
} else {
|
|
56
|
+
favorites.value.add(commandId)
|
|
57
|
+
}
|
|
58
|
+
persistFavorites()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onMounted(() => {
|
|
62
|
+
loadFavorites()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// ─── Filters + view ─────────────────────────────────────────────────────
|
|
10
66
|
|
|
11
67
|
const search = ref('')
|
|
12
68
|
const departmentFilter = ref('all')
|
|
69
|
+
const view = ref<'all' | 'favorites'>('all')
|
|
13
70
|
const page = ref(1)
|
|
14
71
|
const pageSize = 20
|
|
15
72
|
const expandedRow = ref<string | null>(null)
|
|
16
73
|
|
|
74
|
+
const commands = computed(() => data.value?.commands ?? [])
|
|
75
|
+
|
|
17
76
|
const departments = computed(() => {
|
|
18
|
-
const depts = new Set(commands.value.map(c => c.department))
|
|
77
|
+
const depts = new Set(commands.value.map((c) => c.department))
|
|
19
78
|
return [
|
|
20
79
|
{ label: 'All Departments', value: 'all' },
|
|
21
|
-
...Array.from(depts).sort().map(d => ({ label: d, value: d }))
|
|
80
|
+
...Array.from(depts).sort().map((d) => ({ label: d, value: d })),
|
|
22
81
|
]
|
|
23
82
|
})
|
|
24
83
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
84
|
+
const baseList = computed<Command[]>(() => {
|
|
85
|
+
if (view.value === 'favorites') {
|
|
86
|
+
return commands.value.filter((c) => favorites.value.has(c.id))
|
|
87
|
+
}
|
|
88
|
+
return commands.value
|
|
89
|
+
})
|
|
28
90
|
|
|
91
|
+
const filteredCommands = computed<Command[]>(() => {
|
|
92
|
+
let result = baseList.value
|
|
93
|
+
const query = search.value.toLowerCase().trim()
|
|
29
94
|
if (query) {
|
|
30
|
-
result = result.filter(cmd =>
|
|
95
|
+
result = result.filter((cmd) =>
|
|
31
96
|
cmd.command.toLowerCase().includes(query)
|
|
32
|
-
|| cmd.description.toLowerCase().includes(query)
|
|
97
|
+
|| cmd.description.toLowerCase().includes(query),
|
|
33
98
|
)
|
|
34
99
|
}
|
|
35
|
-
|
|
36
100
|
if (departmentFilter.value !== 'all') {
|
|
37
|
-
result = result.filter(cmd => cmd.department === departmentFilter.value)
|
|
101
|
+
result = result.filter((cmd) => cmd.department === departmentFilter.value)
|
|
102
|
+
}
|
|
103
|
+
// Favorites pinned on top in the "all" view; the favorites-only
|
|
104
|
+
// view doesn't need re-sorting.
|
|
105
|
+
if (view.value === 'all') {
|
|
106
|
+
result = [...result].sort((a, b) => {
|
|
107
|
+
const aFav = favorites.value.has(a.id) ? 0 : 1
|
|
108
|
+
const bFav = favorites.value.has(b.id) ? 0 : 1
|
|
109
|
+
return aFav - bFav
|
|
110
|
+
})
|
|
38
111
|
}
|
|
39
|
-
|
|
40
112
|
return result
|
|
41
113
|
})
|
|
42
114
|
|
|
43
115
|
const totalFiltered = computed(() => filteredCommands.value.length)
|
|
44
116
|
|
|
45
|
-
const paginatedCommands = computed(() =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
117
|
+
const paginatedCommands = computed(() =>
|
|
118
|
+
filteredCommands.value.slice(
|
|
119
|
+
(page.value - 1) * pageSize,
|
|
120
|
+
page.value * pageSize,
|
|
121
|
+
),
|
|
122
|
+
)
|
|
49
123
|
|
|
50
|
-
const totalPages = computed(() =>
|
|
124
|
+
const totalPages = computed(() =>
|
|
125
|
+
Math.max(1, Math.ceil(totalFiltered.value / pageSize)),
|
|
126
|
+
)
|
|
51
127
|
|
|
52
|
-
watch([search, departmentFilter], () => {
|
|
128
|
+
watch([search, departmentFilter, view], () => {
|
|
53
129
|
page.value = 1
|
|
54
130
|
})
|
|
55
131
|
|
|
132
|
+
// ─── Copy command to clipboard ──────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
const copied = ref<string | null>(null)
|
|
135
|
+
let copyTimer: ReturnType<typeof setTimeout> | null = null
|
|
136
|
+
|
|
137
|
+
async function copyCommand(cmd: Command) {
|
|
138
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
|
139
|
+
toast.add({
|
|
140
|
+
title: 'Clipboard unavailable',
|
|
141
|
+
description: 'Your browser blocked navigator.clipboard.',
|
|
142
|
+
color: 'warning',
|
|
143
|
+
})
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
await navigator.clipboard.writeText(cmd.command)
|
|
148
|
+
copied.value = cmd.id
|
|
149
|
+
if (copyTimer) clearTimeout(copyTimer)
|
|
150
|
+
copyTimer = setTimeout(() => {
|
|
151
|
+
copied.value = null
|
|
152
|
+
copyTimer = null
|
|
153
|
+
}, 1500)
|
|
154
|
+
toast.add({
|
|
155
|
+
title: 'Copied',
|
|
156
|
+
description: cmd.command,
|
|
157
|
+
color: 'success',
|
|
158
|
+
})
|
|
159
|
+
} catch (err) {
|
|
160
|
+
toast.add({
|
|
161
|
+
title: 'Copy failed',
|
|
162
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
163
|
+
color: 'error',
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
onBeforeUnmount(() => {
|
|
169
|
+
if (copyTimer) clearTimeout(copyTimer)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// ─── Expansion + table ──────────────────────────────────────────────────
|
|
173
|
+
|
|
56
174
|
function toggleExpand(commandId: string) {
|
|
57
175
|
expandedRow.value = expandedRow.value === commandId ? null : commandId
|
|
58
176
|
}
|
|
59
177
|
|
|
60
178
|
const columns: TableColumn<Command>[] = [
|
|
61
|
-
{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
header: 'Description'
|
|
72
|
-
}
|
|
179
|
+
{ accessorKey: 'star', header: '' },
|
|
180
|
+
{ accessorKey: 'command', header: 'Command' },
|
|
181
|
+
{ accessorKey: 'department', header: 'Department' },
|
|
182
|
+
{ accessorKey: 'description', header: 'Description' },
|
|
183
|
+
{ accessorKey: 'actions', header: '' },
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
const viewTabs = [
|
|
187
|
+
{ label: 'All', value: 'all' as const },
|
|
188
|
+
{ label: 'Favorites', value: 'favorites' as const },
|
|
73
189
|
]
|
|
190
|
+
|
|
191
|
+
const favoritesCount = computed(() => favorites.value.size)
|
|
74
192
|
</script>
|
|
75
193
|
|
|
76
194
|
<template>
|
|
@@ -81,33 +199,43 @@ const columns: TableColumn<Command>[] = [
|
|
|
81
199
|
<UDashboardSidebarCollapse />
|
|
82
200
|
</template>
|
|
83
201
|
<template #trailing>
|
|
84
|
-
<UBadge
|
|
202
|
+
<UBadge
|
|
203
|
+
v-if="data?.total"
|
|
204
|
+
:label="`${data.total} total`"
|
|
205
|
+
variant="subtle"
|
|
206
|
+
size="xs"
|
|
207
|
+
/>
|
|
208
|
+
<UBadge
|
|
209
|
+
v-if="favoritesCount"
|
|
210
|
+
:label="`★ ${favoritesCount}`"
|
|
211
|
+
variant="subtle"
|
|
212
|
+
color="warning"
|
|
213
|
+
size="xs"
|
|
214
|
+
class="ml-2"
|
|
215
|
+
/>
|
|
85
216
|
</template>
|
|
86
217
|
</UDashboardNavbar>
|
|
87
218
|
</template>
|
|
88
219
|
|
|
89
220
|
<template #body>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
</div>
|
|
101
|
-
|
|
102
|
-
<!-- Empty -->
|
|
103
|
-
<div v-else-if="!commands.length" class="flex flex-col items-center justify-center gap-4 py-12">
|
|
104
|
-
<UIcon name="i-lucide-terminal" class="size-12 text-muted" />
|
|
105
|
-
<p class="text-sm text-muted">No commands found.</p>
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
<!-- Content -->
|
|
109
|
-
<template v-else>
|
|
221
|
+
<DashboardState
|
|
222
|
+
:status="status"
|
|
223
|
+
:error="error"
|
|
224
|
+
:empty="!commands.length"
|
|
225
|
+
empty-title="No commands found"
|
|
226
|
+
empty-icon="i-lucide-terminal"
|
|
227
|
+
loading-label="Loading commands"
|
|
228
|
+
:on-retry="() => refresh()"
|
|
229
|
+
>
|
|
230
|
+
<!-- View tabs + filters -->
|
|
110
231
|
<div class="flex flex-wrap items-center gap-3 mb-4">
|
|
232
|
+
<UTabs
|
|
233
|
+
:items="viewTabs"
|
|
234
|
+
:model-value="view"
|
|
235
|
+
class="shrink-0"
|
|
236
|
+
@update:model-value="view = $event as 'all' | 'favorites'"
|
|
237
|
+
/>
|
|
238
|
+
|
|
111
239
|
<UInput
|
|
112
240
|
v-model="search"
|
|
113
241
|
class="max-w-sm"
|
|
@@ -130,51 +258,98 @@ const columns: TableColumn<Command>[] = [
|
|
|
130
258
|
</span>
|
|
131
259
|
</div>
|
|
132
260
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
class="shrink-0"
|
|
138
|
-
:ui="{
|
|
139
|
-
base: 'table-fixed border-separate border-spacing-0',
|
|
140
|
-
thead: '[&>tr]:bg-elevated/50 [&>tr]:after:content-none',
|
|
141
|
-
tbody: '[&>tr]:last:[&>td]:border-b-0 [&>tr]:cursor-pointer [&>tr]:hover:bg-elevated/50 [&>tr]:transition-colors',
|
|
142
|
-
th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
|
|
143
|
-
td: 'border-b border-default'
|
|
144
|
-
}"
|
|
145
|
-
@select="(row: Command) => toggleExpand(row.id)"
|
|
261
|
+
<!-- Favorites empty state -->
|
|
262
|
+
<div
|
|
263
|
+
v-if="view === 'favorites' && favoritesCount === 0"
|
|
264
|
+
class="flex flex-col items-center justify-center gap-3 py-16 rounded-lg border border-default"
|
|
146
265
|
>
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
</template>
|
|
153
|
-
<template #expanded="{ row }">
|
|
154
|
-
<div v-if="expandedRow === row.original.id && row.original.keywords?.length" class="px-4 py-3 bg-elevated/30">
|
|
155
|
-
<p class="text-xs font-semibold text-muted uppercase tracking-wider mb-2">Keywords</p>
|
|
156
|
-
<div class="flex flex-wrap gap-1.5">
|
|
157
|
-
<UBadge
|
|
158
|
-
v-for="kw in row.original.keywords"
|
|
159
|
-
:key="kw"
|
|
160
|
-
:label="kw"
|
|
161
|
-
variant="outline"
|
|
162
|
-
size="xs"
|
|
163
|
-
/>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
</template>
|
|
167
|
-
</UTable>
|
|
168
|
-
|
|
169
|
-
<div v-if="totalPages > 1" class="flex items-center justify-center mt-6">
|
|
170
|
-
<UPagination
|
|
171
|
-
:page="page"
|
|
172
|
-
:total="totalFiltered"
|
|
173
|
-
:items-per-page="pageSize"
|
|
174
|
-
@update:page="(val) => page = val"
|
|
175
|
-
/>
|
|
266
|
+
<UIcon name="i-lucide-star" class="size-12 text-muted" />
|
|
267
|
+
<p class="text-sm text-muted">No favorites yet.</p>
|
|
268
|
+
<p class="text-xs text-muted text-center max-w-sm">
|
|
269
|
+
Click the ★ next to any command to pin it here for one-click access.
|
|
270
|
+
</p>
|
|
176
271
|
</div>
|
|
177
|
-
|
|
272
|
+
|
|
273
|
+
<!-- Table -->
|
|
274
|
+
<template v-else>
|
|
275
|
+
<UTable
|
|
276
|
+
:data="paginatedCommands"
|
|
277
|
+
:columns="columns"
|
|
278
|
+
:loading="status === 'pending'"
|
|
279
|
+
class="shrink-0"
|
|
280
|
+
:ui="{
|
|
281
|
+
base: 'table-fixed border-separate border-spacing-0',
|
|
282
|
+
thead: '[&>tr]:bg-elevated/50 [&>tr]:after:content-none',
|
|
283
|
+
tbody: '[&>tr]:last:[&>td]:border-b-0 [&>tr]:hover:bg-elevated/50 [&>tr]:transition-colors',
|
|
284
|
+
th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
|
|
285
|
+
td: 'border-b border-default',
|
|
286
|
+
}"
|
|
287
|
+
>
|
|
288
|
+
<template #star-cell="{ row }">
|
|
289
|
+
<UButton
|
|
290
|
+
:icon="favorites.has(row.original.id) ? 'i-lucide-star' : 'i-lucide-star'"
|
|
291
|
+
:color="favorites.has(row.original.id) ? 'warning' : 'neutral'"
|
|
292
|
+
variant="ghost"
|
|
293
|
+
size="xs"
|
|
294
|
+
:aria-label="favorites.has(row.original.id) ? 'Unfavorite' : 'Favorite'"
|
|
295
|
+
:class="favorites.has(row.original.id) ? '' : 'opacity-30 hover:opacity-100'"
|
|
296
|
+
@click.stop="toggleFavorite(row.original.id)"
|
|
297
|
+
/>
|
|
298
|
+
</template>
|
|
299
|
+
<template #command-cell="{ row }">
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
class="text-left w-full"
|
|
303
|
+
@click="toggleExpand(row.original.id)"
|
|
304
|
+
>
|
|
305
|
+
<code class="font-mono text-sm text-primary">{{ row.original.command }}</code>
|
|
306
|
+
</button>
|
|
307
|
+
</template>
|
|
308
|
+
<template #department-cell="{ row }">
|
|
309
|
+
<UBadge :label="row.original.department" variant="subtle" size="sm" />
|
|
310
|
+
</template>
|
|
311
|
+
<template #description-cell="{ row }">
|
|
312
|
+
<span class="text-sm text-muted">{{ row.original.description }}</span>
|
|
313
|
+
</template>
|
|
314
|
+
<template #actions-cell="{ row }">
|
|
315
|
+
<UButton
|
|
316
|
+
:icon="copied === row.original.id ? 'i-lucide-check' : 'i-lucide-copy'"
|
|
317
|
+
:color="copied === row.original.id ? 'success' : 'neutral'"
|
|
318
|
+
variant="ghost"
|
|
319
|
+
size="xs"
|
|
320
|
+
aria-label="Copy command to clipboard"
|
|
321
|
+
@click.stop="copyCommand(row.original)"
|
|
322
|
+
/>
|
|
323
|
+
</template>
|
|
324
|
+
<template #expanded="{ row }">
|
|
325
|
+
<div
|
|
326
|
+
v-if="expandedRow === row.original.id && row.original.keywords?.length"
|
|
327
|
+
class="px-4 py-3 bg-elevated/30"
|
|
328
|
+
>
|
|
329
|
+
<p class="text-xs font-semibold text-muted uppercase tracking-wider mb-2">Keywords</p>
|
|
330
|
+
<div class="flex flex-wrap gap-1.5">
|
|
331
|
+
<UBadge
|
|
332
|
+
v-for="kw in row.original.keywords"
|
|
333
|
+
:key="kw"
|
|
334
|
+
:label="kw"
|
|
335
|
+
variant="outline"
|
|
336
|
+
size="xs"
|
|
337
|
+
/>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</template>
|
|
341
|
+
</UTable>
|
|
342
|
+
|
|
343
|
+
<div v-if="totalPages > 1" class="flex items-center justify-center mt-6">
|
|
344
|
+
<UPagination
|
|
345
|
+
:page="page"
|
|
346
|
+
:total="totalFiltered"
|
|
347
|
+
:items-per-page="pageSize"
|
|
348
|
+
@update:page="(val) => page = val"
|
|
349
|
+
/>
|
|
350
|
+
</div>
|
|
351
|
+
</template>
|
|
352
|
+
</DashboardState>
|
|
178
353
|
</template>
|
|
179
354
|
</UDashboardPanel>
|
|
180
355
|
</template>
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -173,6 +173,51 @@ def agents(dept: Optional[str] = Query(None)):
|
|
|
173
173
|
return {"agents": data, "total": len(data)}
|
|
174
174
|
|
|
175
175
|
|
|
176
|
+
@app.get("/api/agents/activity")
|
|
177
|
+
def agents_activity(period: str = "week"):
|
|
178
|
+
"""Per-department activity from the PR47 LLM cost telemetry.
|
|
179
|
+
|
|
180
|
+
Returns ``{by_department: {dev: {call_count, total_cost_usd,
|
|
181
|
+
total_tokens_in, total_tokens_out}}}`` derived from rows whose
|
|
182
|
+
``category`` field starts with ``subagent:``. Each agent's
|
|
183
|
+
dispatch is currently tagged at the department level — finer
|
|
184
|
+
per-agent attribution will land when orchestrators set
|
|
185
|
+
``ARKA_CALL_CATEGORY=subagent:<dept>:<agent>``.
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
from core.runtime.llm_cost_telemetry import summarise, VALID_PERIODS
|
|
189
|
+
except Exception: # pragma: no cover - import guard
|
|
190
|
+
return {"by_department": {}, "period": period}
|
|
191
|
+
if period not in VALID_PERIODS:
|
|
192
|
+
period = "week"
|
|
193
|
+
summary = summarise(period=period)
|
|
194
|
+
out: dict[str, dict] = {}
|
|
195
|
+
for category, row in (summary.by_category or {}).items():
|
|
196
|
+
if not isinstance(category, str) or not category.startswith("subagent:"):
|
|
197
|
+
continue
|
|
198
|
+
dept = category.split(":", 1)[1] or "unknown"
|
|
199
|
+
bucket = out.setdefault(dept, {
|
|
200
|
+
"call_count": 0,
|
|
201
|
+
"total_cost_usd": 0.0,
|
|
202
|
+
"any_cost_known": False,
|
|
203
|
+
"total_tokens_in": 0,
|
|
204
|
+
"total_tokens_out": 0,
|
|
205
|
+
})
|
|
206
|
+
bucket["call_count"] += row.get("call_count", 0)
|
|
207
|
+
bucket["total_tokens_in"] += row.get("total_tokens_in", 0)
|
|
208
|
+
bucket["total_tokens_out"] += row.get("total_tokens_out", 0)
|
|
209
|
+
cost = row.get("total_cost_usd")
|
|
210
|
+
if isinstance(cost, (int, float)):
|
|
211
|
+
bucket["total_cost_usd"] += float(cost)
|
|
212
|
+
bucket["any_cost_known"] = True
|
|
213
|
+
for dept, b in out.items():
|
|
214
|
+
if not b.pop("any_cost_known"):
|
|
215
|
+
b["total_cost_usd"] = None
|
|
216
|
+
else:
|
|
217
|
+
b["total_cost_usd"] = round(b["total_cost_usd"], 6)
|
|
218
|
+
return {"by_department": out, "period": period}
|
|
219
|
+
|
|
220
|
+
|
|
176
221
|
@app.get("/api/agents/{agent_id}")
|
|
177
222
|
def agent_detail(agent_id: str):
|
|
178
223
|
"""Get full agent detail including YAML data."""
|