arkaos 3.22.0 → 3.24.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 +1 -1
- package/dashboard/app/layouts/default.vue +7 -0
- package/dashboard/app/pages/personas/[id].vue +23 -0
- package/dashboard/app/pages/personas/compare-with-agent.vue +218 -0
- package/dashboard/app/pages/workflows.vue +191 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +40 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.24.0
|
|
@@ -48,6 +48,13 @@ const links = [[{
|
|
|
48
48
|
onSelect: () => {
|
|
49
49
|
open.value = false
|
|
50
50
|
}
|
|
51
|
+
}, {
|
|
52
|
+
label: 'Workflows',
|
|
53
|
+
icon: 'i-lucide-workflow',
|
|
54
|
+
to: '/workflows',
|
|
55
|
+
onSelect: () => {
|
|
56
|
+
open.value = false
|
|
57
|
+
}
|
|
51
58
|
}, {
|
|
52
59
|
label: 'Knowledge',
|
|
53
60
|
icon: 'i-lucide-brain',
|
|
@@ -247,6 +247,17 @@ function onCloned(agentId: string) {
|
|
|
247
247
|
navigateTo(`/agents/${agentId}`)
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
// PR88a v3.23.0 — Compare with linked agent.
|
|
251
|
+
const compareWithOptions = computed(() =>
|
|
252
|
+
linkedAgentIds.value.map((aid) => ({
|
|
253
|
+
label: `Compare with ${aid}`,
|
|
254
|
+
icon: 'i-lucide-columns-2',
|
|
255
|
+
onSelect: () => navigateTo(
|
|
256
|
+
`/personas/compare-with-agent?persona=${personaId}&agent=${aid}`,
|
|
257
|
+
),
|
|
258
|
+
})),
|
|
259
|
+
)
|
|
260
|
+
|
|
250
261
|
// PR84c v3.9.0 — Auto-fill empty lists in one go.
|
|
251
262
|
const autofilling = ref(false)
|
|
252
263
|
|
|
@@ -496,6 +507,18 @@ const vocabOptions = [
|
|
|
496
507
|
:aria-label="favs.isPersonaFavorite(detail.id) ? 'Unfavorite' : 'Favorite'"
|
|
497
508
|
@click="favs.toggle('personas', detail.id)"
|
|
498
509
|
/>
|
|
510
|
+
<UDropdownMenu
|
|
511
|
+
v-if="compareWithOptions.length > 0"
|
|
512
|
+
:items="compareWithOptions"
|
|
513
|
+
>
|
|
514
|
+
<UButton
|
|
515
|
+
label="Compare"
|
|
516
|
+
icon="i-lucide-columns-2"
|
|
517
|
+
variant="soft"
|
|
518
|
+
size="sm"
|
|
519
|
+
trailing-icon="i-lucide-chevron-down"
|
|
520
|
+
/>
|
|
521
|
+
</UDropdownMenu>
|
|
499
522
|
<UButton
|
|
500
523
|
label="Clone to Agent"
|
|
501
524
|
icon="i-lucide-copy-plus"
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR88a v3.23.0 — Compare a persona against one of its linked agents.
|
|
3
|
+
//
|
|
4
|
+
// Driven by `?persona=p&agent=a`. Useful to see how an agent diverged
|
|
5
|
+
// from the persona it was cloned from (or whose DNA inspired it).
|
|
6
|
+
|
|
7
|
+
const route = useRoute()
|
|
8
|
+
const { fetchApi } = useApi()
|
|
9
|
+
|
|
10
|
+
const personaId = computed(() => String(route.query.persona ?? ''))
|
|
11
|
+
const agentId = computed(() => String(route.query.agent ?? ''))
|
|
12
|
+
|
|
13
|
+
interface PersonaDetail {
|
|
14
|
+
id: string
|
|
15
|
+
name?: string
|
|
16
|
+
title?: string
|
|
17
|
+
source?: string
|
|
18
|
+
mbti?: string
|
|
19
|
+
disc?: { primary?: string, secondary?: string }
|
|
20
|
+
enneagram?: { type?: number, wing?: number }
|
|
21
|
+
big_five?: {
|
|
22
|
+
openness?: number
|
|
23
|
+
conscientiousness?: number
|
|
24
|
+
extraversion?: number
|
|
25
|
+
agreeableness?: number
|
|
26
|
+
neuroticism?: number
|
|
27
|
+
}
|
|
28
|
+
mental_models?: string[]
|
|
29
|
+
expertise_domains?: string[]
|
|
30
|
+
frameworks?: string[]
|
|
31
|
+
communication?: { tone?: string, vocabulary_level?: string, avoid?: string[] }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface AgentDetail {
|
|
35
|
+
id: string
|
|
36
|
+
name?: string
|
|
37
|
+
role?: string
|
|
38
|
+
department?: string
|
|
39
|
+
mbti?: string
|
|
40
|
+
disc?: { primary?: string, secondary?: string }
|
|
41
|
+
enneagram?: { type?: number, wing?: number }
|
|
42
|
+
big_five?: PersonaDetail['big_five']
|
|
43
|
+
mental_models?: { primary?: string[], secondary?: string[] }
|
|
44
|
+
expertise?: { domains?: string[], frameworks?: string[] }
|
|
45
|
+
communication?: { tone?: string, vocabulary_level?: string, avoid?: string[] }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { data: persona, status: pStatus } = fetchApi<PersonaDetail>(
|
|
49
|
+
() => personaId.value ? `/api/personas/${personaId.value}` : '',
|
|
50
|
+
)
|
|
51
|
+
const { data: agent, status: aStatus } = fetchApi<AgentDetail>(
|
|
52
|
+
() => agentId.value ? `/api/agents/${agentId.value}` : '',
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const loading = computed(() => pStatus.value === 'pending' || aStatus.value === 'pending')
|
|
56
|
+
const errorMsg = computed(() => {
|
|
57
|
+
if (!personaId.value || !agentId.value) {
|
|
58
|
+
return 'Pass ?persona=p&agent=a'
|
|
59
|
+
}
|
|
60
|
+
if (persona.value && (persona.value as any).error) return `Persona: ${(persona.value as any).error}`
|
|
61
|
+
if (agent.value && (agent.value as any).error) return `Agent: ${(agent.value as any).error}`
|
|
62
|
+
return null
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
function diffClass(left: unknown, right: unknown): string {
|
|
66
|
+
return left !== right
|
|
67
|
+
? 'bg-yellow-500/10 border-yellow-500/30'
|
|
68
|
+
: ''
|
|
69
|
+
}
|
|
70
|
+
function listDiffClass(left: unknown[] | undefined, right: unknown[] | undefined): string {
|
|
71
|
+
const a = JSON.stringify([...(left ?? [])].sort())
|
|
72
|
+
const b = JSON.stringify([...(right ?? [])].sort())
|
|
73
|
+
return a !== b ? 'bg-yellow-500/10 border-yellow-500/30' : ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism'] as const
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<template>
|
|
80
|
+
<UDashboardPanel id="persona-vs-agent">
|
|
81
|
+
<template #header>
|
|
82
|
+
<UDashboardNavbar title="Persona vs Agent">
|
|
83
|
+
<template #leading>
|
|
84
|
+
<UButton
|
|
85
|
+
v-if="personaId"
|
|
86
|
+
icon="i-lucide-arrow-left"
|
|
87
|
+
variant="ghost"
|
|
88
|
+
size="sm"
|
|
89
|
+
:to="`/personas/${personaId}`"
|
|
90
|
+
aria-label="Back to persona"
|
|
91
|
+
/>
|
|
92
|
+
</template>
|
|
93
|
+
<template #trailing>
|
|
94
|
+
<UBadge label="diverge view" variant="subtle" size="sm" />
|
|
95
|
+
</template>
|
|
96
|
+
</UDashboardNavbar>
|
|
97
|
+
</template>
|
|
98
|
+
|
|
99
|
+
<template #body>
|
|
100
|
+
<div v-if="errorMsg" class="p-6 text-center text-sm text-error">
|
|
101
|
+
{{ errorMsg }}
|
|
102
|
+
</div>
|
|
103
|
+
<div v-else-if="loading" class="p-6 text-center text-sm text-muted">
|
|
104
|
+
<UIcon name="i-lucide-loader-2" class="size-4 animate-spin inline" /> Loading…
|
|
105
|
+
</div>
|
|
106
|
+
<div v-else-if="persona && agent" class="space-y-4 max-w-6xl">
|
|
107
|
+
<section class="grid grid-cols-2 gap-3">
|
|
108
|
+
<NuxtLink :to="`/personas/${persona.id}`" class="rounded-lg border border-default p-4 hover:border-primary/40">
|
|
109
|
+
<p class="text-xs text-muted uppercase tracking-wide">Persona</p>
|
|
110
|
+
<h2 class="text-xl font-bold">{{ persona.name }}</h2>
|
|
111
|
+
<p class="text-sm text-muted">{{ persona.title || '—' }}</p>
|
|
112
|
+
</NuxtLink>
|
|
113
|
+
<NuxtLink :to="`/agents/${agent.id}`" class="rounded-lg border border-default p-4 hover:border-primary/40">
|
|
114
|
+
<p class="text-xs text-muted uppercase tracking-wide">Agent</p>
|
|
115
|
+
<h2 class="text-xl font-bold">{{ agent.name }}</h2>
|
|
116
|
+
<p class="text-sm text-muted">{{ agent.role }} · {{ agent.department }}</p>
|
|
117
|
+
</NuxtLink>
|
|
118
|
+
</section>
|
|
119
|
+
|
|
120
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Behavioural DNA</h3>
|
|
121
|
+
<div class="grid grid-cols-2 gap-3">
|
|
122
|
+
<div :class="['rounded-lg border p-3', diffClass(persona.mbti, agent.mbti)]">
|
|
123
|
+
<p class="text-xs text-muted">MBTI</p>
|
|
124
|
+
<p class="text-lg font-mono font-bold">{{ persona.mbti ?? '—' }}</p>
|
|
125
|
+
</div>
|
|
126
|
+
<div :class="['rounded-lg border p-3', diffClass(persona.mbti, agent.mbti)]">
|
|
127
|
+
<p class="text-xs text-muted">MBTI</p>
|
|
128
|
+
<p class="text-lg font-mono font-bold">{{ agent.mbti ?? '—' }}</p>
|
|
129
|
+
</div>
|
|
130
|
+
<div :class="['rounded-lg border p-3', diffClass(`${persona.disc?.primary}/${persona.disc?.secondary}`, `${agent.disc?.primary}/${agent.disc?.secondary}`)]">
|
|
131
|
+
<p class="text-xs text-muted">DISC</p>
|
|
132
|
+
<p class="text-lg font-mono font-bold">{{ persona.disc?.primary ?? '?' }}/{{ persona.disc?.secondary ?? '?' }}</p>
|
|
133
|
+
</div>
|
|
134
|
+
<div :class="['rounded-lg border p-3', diffClass(`${persona.disc?.primary}/${persona.disc?.secondary}`, `${agent.disc?.primary}/${agent.disc?.secondary}`)]">
|
|
135
|
+
<p class="text-xs text-muted">DISC</p>
|
|
136
|
+
<p class="text-lg font-mono font-bold">{{ agent.disc?.primary ?? '?' }}/{{ agent.disc?.secondary ?? '?' }}</p>
|
|
137
|
+
</div>
|
|
138
|
+
<div :class="['rounded-lg border p-3', diffClass(`${persona.enneagram?.type}w${persona.enneagram?.wing}`, `${agent.enneagram?.type}w${agent.enneagram?.wing}`)]">
|
|
139
|
+
<p class="text-xs text-muted">Enneagram</p>
|
|
140
|
+
<p class="text-lg font-mono font-bold">{{ persona.enneagram?.type ?? '?' }}w{{ persona.enneagram?.wing ?? '?' }}</p>
|
|
141
|
+
</div>
|
|
142
|
+
<div :class="['rounded-lg border p-3', diffClass(`${persona.enneagram?.type}w${persona.enneagram?.wing}`, `${agent.enneagram?.type}w${agent.enneagram?.wing}`)]">
|
|
143
|
+
<p class="text-xs text-muted">Enneagram</p>
|
|
144
|
+
<p class="text-lg font-mono font-bold">{{ agent.enneagram?.type ?? '?' }}w{{ agent.enneagram?.wing ?? '?' }}</p>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Big Five (OCEAN)</h3>
|
|
149
|
+
<div class="space-y-1">
|
|
150
|
+
<div v-for="k in bigFiveKeys" :key="k" class="grid grid-cols-2 gap-3">
|
|
151
|
+
<div :class="['rounded-lg border p-2 flex items-center gap-3', diffClass(persona.big_five?.[k], agent.big_five?.[k])]">
|
|
152
|
+
<span class="text-xs text-muted w-36 shrink-0 capitalize">{{ k }}</span>
|
|
153
|
+
<span class="font-mono text-sm">{{ persona.big_five?.[k] ?? '—' }}</span>
|
|
154
|
+
</div>
|
|
155
|
+
<div :class="['rounded-lg border p-2 flex items-center gap-3', diffClass(persona.big_five?.[k], agent.big_five?.[k])]">
|
|
156
|
+
<span class="text-xs text-muted w-36 shrink-0 capitalize">{{ k }}</span>
|
|
157
|
+
<span class="font-mono text-sm">{{ agent.big_five?.[k] ?? '—' }}</span>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Expertise domains</h3>
|
|
163
|
+
<div class="grid grid-cols-2 gap-3">
|
|
164
|
+
<div :class="['rounded-lg border p-3', listDiffClass(persona.expertise_domains, agent.expertise?.domains)]">
|
|
165
|
+
<ul class="list-disc list-inside text-sm space-y-1">
|
|
166
|
+
<li v-for="d in persona.expertise_domains" :key="d">{{ d }}</li>
|
|
167
|
+
<li v-if="!persona.expertise_domains?.length" class="list-none text-muted italic">none</li>
|
|
168
|
+
</ul>
|
|
169
|
+
</div>
|
|
170
|
+
<div :class="['rounded-lg border p-3', listDiffClass(persona.expertise_domains, agent.expertise?.domains)]">
|
|
171
|
+
<ul class="list-disc list-inside text-sm space-y-1">
|
|
172
|
+
<li v-for="d in agent.expertise?.domains" :key="d">{{ d }}</li>
|
|
173
|
+
<li v-if="!agent.expertise?.domains?.length" class="list-none text-muted italic">none</li>
|
|
174
|
+
</ul>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Frameworks</h3>
|
|
179
|
+
<div class="grid grid-cols-2 gap-3">
|
|
180
|
+
<div :class="['rounded-lg border p-3', listDiffClass(persona.frameworks, agent.expertise?.frameworks)]">
|
|
181
|
+
<ul class="list-disc list-inside text-sm space-y-1">
|
|
182
|
+
<li v-for="f in persona.frameworks" :key="f">{{ f }}</li>
|
|
183
|
+
<li v-if="!persona.frameworks?.length" class="list-none text-muted italic">none</li>
|
|
184
|
+
</ul>
|
|
185
|
+
</div>
|
|
186
|
+
<div :class="['rounded-lg border p-3', listDiffClass(persona.frameworks, agent.expertise?.frameworks)]">
|
|
187
|
+
<ul class="list-disc list-inside text-sm space-y-1">
|
|
188
|
+
<li v-for="f in agent.expertise?.frameworks" :key="f">{{ f }}</li>
|
|
189
|
+
<li v-if="!agent.expertise?.frameworks?.length" class="list-none text-muted italic">none</li>
|
|
190
|
+
</ul>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Mental models</h3>
|
|
195
|
+
<div class="grid grid-cols-2 gap-3">
|
|
196
|
+
<div :class="['rounded-lg border p-3', listDiffClass(persona.mental_models, agent.mental_models?.primary)]">
|
|
197
|
+
<p class="text-xs text-muted mb-2">Persona — flat list</p>
|
|
198
|
+
<ul class="list-disc list-inside text-sm space-y-1">
|
|
199
|
+
<li v-for="m in persona.mental_models" :key="m">{{ m }}</li>
|
|
200
|
+
<li v-if="!persona.mental_models?.length" class="list-none text-muted italic">none</li>
|
|
201
|
+
</ul>
|
|
202
|
+
</div>
|
|
203
|
+
<div :class="['rounded-lg border p-3', listDiffClass(persona.mental_models, agent.mental_models?.primary)]">
|
|
204
|
+
<p class="text-xs text-muted mb-2">Agent — primary</p>
|
|
205
|
+
<ul class="list-disc list-inside text-sm space-y-1">
|
|
206
|
+
<li v-for="m in agent.mental_models?.primary" :key="m">{{ m }}</li>
|
|
207
|
+
<li v-if="!agent.mental_models?.primary?.length" class="list-none text-muted italic">none</li>
|
|
208
|
+
</ul>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<p class="text-xs text-muted pt-4 italic">
|
|
213
|
+
Cells with a yellow tint differ between persona and agent.
|
|
214
|
+
</p>
|
|
215
|
+
</div>
|
|
216
|
+
</template>
|
|
217
|
+
</UDashboardPanel>
|
|
218
|
+
</template>
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR88b v3.24.0 — Workflows browser.
|
|
3
|
+
//
|
|
4
|
+
// Lists every YAML workflow under departments/*/workflows/*.yaml with
|
|
5
|
+
// metadata pulled from the file's frontmatter-ish top keys. Selecting
|
|
6
|
+
// a row opens a side panel with the raw YAML for inspection.
|
|
7
|
+
|
|
8
|
+
import type { TableColumn } from '@nuxt/ui'
|
|
9
|
+
|
|
10
|
+
interface Workflow {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
description: string
|
|
14
|
+
department: string
|
|
15
|
+
tier: string
|
|
16
|
+
command: string
|
|
17
|
+
phases_count: number
|
|
18
|
+
file: string
|
|
19
|
+
content: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { fetchApi } = useApi()
|
|
23
|
+
const { data, status, error, refresh } = await fetchApi<{ workflows: Workflow[] }>('/api/workflows')
|
|
24
|
+
|
|
25
|
+
const workflows = computed(() => data.value?.workflows ?? [])
|
|
26
|
+
const search = ref('')
|
|
27
|
+
const deptFilter = ref<'all' | string>('all')
|
|
28
|
+
const selected = ref<Workflow | null>(null)
|
|
29
|
+
|
|
30
|
+
const departments = computed(() => {
|
|
31
|
+
const set = new Set<string>()
|
|
32
|
+
for (const w of workflows.value) if (w.department) set.add(w.department)
|
|
33
|
+
return [
|
|
34
|
+
{ label: 'All departments', value: 'all' },
|
|
35
|
+
...Array.from(set).sort().map((d) => ({ label: d, value: d })),
|
|
36
|
+
]
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const filtered = computed(() => {
|
|
40
|
+
let result = workflows.value
|
|
41
|
+
if (deptFilter.value !== 'all') {
|
|
42
|
+
result = result.filter((w) => w.department === deptFilter.value)
|
|
43
|
+
}
|
|
44
|
+
const q = search.value.toLowerCase().trim()
|
|
45
|
+
if (q) {
|
|
46
|
+
result = result.filter((w) =>
|
|
47
|
+
w.name.toLowerCase().includes(q)
|
|
48
|
+
|| w.description.toLowerCase().includes(q)
|
|
49
|
+
|| w.command.toLowerCase().includes(q)
|
|
50
|
+
|| w.id.toLowerCase().includes(q),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
return result
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const tierColor = (tier: string) => {
|
|
57
|
+
const m: Record<string, 'primary' | 'success' | 'warning' | 'neutral'> = {
|
|
58
|
+
enterprise: 'primary',
|
|
59
|
+
focused: 'success',
|
|
60
|
+
specialist: 'warning',
|
|
61
|
+
}
|
|
62
|
+
return m[tier] ?? 'neutral'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const columns: TableColumn<Workflow>[] = [
|
|
66
|
+
{ accessorKey: 'name', header: 'Name' },
|
|
67
|
+
{ accessorKey: 'department', header: 'Department' },
|
|
68
|
+
{ accessorKey: 'tier', header: 'Tier' },
|
|
69
|
+
{ accessorKey: 'command', header: 'Command' },
|
|
70
|
+
{ accessorKey: 'phases_count', header: 'Phases' },
|
|
71
|
+
]
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<UDashboardPanel id="workflows">
|
|
76
|
+
<template #header>
|
|
77
|
+
<UDashboardNavbar title="Workflows">
|
|
78
|
+
<template #leading>
|
|
79
|
+
<UDashboardSidebarCollapse />
|
|
80
|
+
</template>
|
|
81
|
+
<template #trailing>
|
|
82
|
+
<UBadge v-if="workflows.length" :label="String(workflows.length)" variant="subtle" />
|
|
83
|
+
</template>
|
|
84
|
+
</UDashboardNavbar>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<template #body>
|
|
88
|
+
<DashboardState
|
|
89
|
+
:status="status"
|
|
90
|
+
:error="error"
|
|
91
|
+
:empty="!workflows.length"
|
|
92
|
+
empty-title="No workflows found"
|
|
93
|
+
empty-description="YAML workflows live under departments/*/workflows/."
|
|
94
|
+
empty-icon="i-lucide-workflow"
|
|
95
|
+
loading-label="Scanning workflows"
|
|
96
|
+
:on-retry="() => refresh()"
|
|
97
|
+
>
|
|
98
|
+
<div class="flex flex-wrap items-center gap-3 mb-4">
|
|
99
|
+
<UInput
|
|
100
|
+
v-model="search"
|
|
101
|
+
class="max-w-sm"
|
|
102
|
+
icon="i-lucide-search"
|
|
103
|
+
placeholder="Search name, command, description…"
|
|
104
|
+
/>
|
|
105
|
+
<USelect
|
|
106
|
+
v-model="deptFilter"
|
|
107
|
+
:items="departments"
|
|
108
|
+
placeholder="Department"
|
|
109
|
+
class="min-w-44"
|
|
110
|
+
/>
|
|
111
|
+
<span class="ml-auto text-xs text-muted">
|
|
112
|
+
{{ filtered.length }} workflow{{ filtered.length === 1 ? '' : 's' }}
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="grid grid-cols-1 xl:grid-cols-[1.4fr_1fr] gap-4">
|
|
117
|
+
<UTable
|
|
118
|
+
:data="filtered"
|
|
119
|
+
:columns="columns"
|
|
120
|
+
:loading="status === 'pending'"
|
|
121
|
+
:ui="{
|
|
122
|
+
tbody: '[&>tr]:cursor-pointer [&>tr]:hover:bg-elevated/50 [&>tr]:transition-colors',
|
|
123
|
+
th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
|
|
124
|
+
td: 'border-b border-default',
|
|
125
|
+
}"
|
|
126
|
+
@select="(row: { original: Workflow }) => selected = row.original"
|
|
127
|
+
>
|
|
128
|
+
<template #name-cell="{ row }">
|
|
129
|
+
<div class="min-w-0">
|
|
130
|
+
<p class="font-medium truncate">{{ row.original.name }}</p>
|
|
131
|
+
<p class="text-xs text-muted font-mono truncate">{{ row.original.id }}</p>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|
|
134
|
+
<template #department-cell="{ row }">
|
|
135
|
+
<UBadge :label="row.original.department" variant="subtle" size="sm" />
|
|
136
|
+
</template>
|
|
137
|
+
<template #tier-cell="{ row }">
|
|
138
|
+
<UBadge
|
|
139
|
+
v-if="row.original.tier"
|
|
140
|
+
:label="row.original.tier"
|
|
141
|
+
:color="tierColor(row.original.tier)"
|
|
142
|
+
variant="subtle"
|
|
143
|
+
size="sm"
|
|
144
|
+
/>
|
|
145
|
+
<span v-else class="text-xs text-muted">—</span>
|
|
146
|
+
</template>
|
|
147
|
+
<template #command-cell="{ row }">
|
|
148
|
+
<code v-if="row.original.command" class="text-xs font-mono">{{ row.original.command }}</code>
|
|
149
|
+
<span v-else class="text-xs text-muted">—</span>
|
|
150
|
+
</template>
|
|
151
|
+
<template #phases_count-cell="{ row }">
|
|
152
|
+
<span class="font-mono text-sm">{{ row.original.phases_count }}</span>
|
|
153
|
+
</template>
|
|
154
|
+
</UTable>
|
|
155
|
+
|
|
156
|
+
<div
|
|
157
|
+
v-if="selected"
|
|
158
|
+
class="rounded-lg border border-default overflow-hidden flex flex-col"
|
|
159
|
+
>
|
|
160
|
+
<div class="px-4 py-3 border-b border-default bg-elevated/30">
|
|
161
|
+
<div class="flex items-start justify-between gap-3">
|
|
162
|
+
<div class="min-w-0">
|
|
163
|
+
<p class="text-xs text-muted uppercase tracking-wide">YAML preview</p>
|
|
164
|
+
<p class="font-semibold truncate">{{ selected.name }}</p>
|
|
165
|
+
<p class="text-xs text-muted font-mono truncate">{{ selected.file }}</p>
|
|
166
|
+
</div>
|
|
167
|
+
<UButton
|
|
168
|
+
icon="i-lucide-x"
|
|
169
|
+
variant="ghost"
|
|
170
|
+
size="xs"
|
|
171
|
+
aria-label="Close preview"
|
|
172
|
+
@click="selected = null"
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
<p v-if="selected.description" class="text-xs text-muted mt-2">
|
|
176
|
+
{{ selected.description }}
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
<pre class="p-4 text-xs font-mono overflow-x-auto whitespace-pre">{{ selected.content }}</pre>
|
|
180
|
+
</div>
|
|
181
|
+
<div
|
|
182
|
+
v-else
|
|
183
|
+
class="rounded-lg border border-dashed border-default p-8 text-center text-sm text-muted self-start"
|
|
184
|
+
>
|
|
185
|
+
Click a row to preview its YAML.
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</DashboardState>
|
|
189
|
+
</template>
|
|
190
|
+
</UDashboardPanel>
|
|
191
|
+
</template>
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -1387,6 +1387,46 @@ def agent_export_to_vault(agent_id: str):
|
|
|
1387
1387
|
return {"exported": True, "path": str(res.path), "vault_path": str(res.vault_path)}
|
|
1388
1388
|
|
|
1389
1389
|
|
|
1390
|
+
# --- Workflows (PR88b v3.24.0) ---
|
|
1391
|
+
|
|
1392
|
+
@app.get("/api/workflows")
|
|
1393
|
+
def workflows_list():
|
|
1394
|
+
"""Scan departments/*/workflows/*.yaml and return metadata + content."""
|
|
1395
|
+
out: list[dict] = []
|
|
1396
|
+
dept_root = ARKAOS_ROOT / "departments"
|
|
1397
|
+
if not dept_root.exists():
|
|
1398
|
+
return {"workflows": []}
|
|
1399
|
+
try:
|
|
1400
|
+
import yaml as _yaml
|
|
1401
|
+
except ImportError:
|
|
1402
|
+
return {"workflows": [], "error": "PyYAML unavailable"}
|
|
1403
|
+
for path in sorted(dept_root.glob("*/workflows/*.yaml")):
|
|
1404
|
+
try:
|
|
1405
|
+
content = path.read_text(encoding="utf-8")
|
|
1406
|
+
except OSError:
|
|
1407
|
+
continue
|
|
1408
|
+
try:
|
|
1409
|
+
raw = _yaml.safe_load(content) or {}
|
|
1410
|
+
except Exception: # noqa: BLE001
|
|
1411
|
+
raw = {}
|
|
1412
|
+
if not isinstance(raw, dict):
|
|
1413
|
+
continue
|
|
1414
|
+
phases = raw.get("phases") if isinstance(raw.get("phases"), list) else []
|
|
1415
|
+
rel = path.relative_to(ARKAOS_ROOT).as_posix()
|
|
1416
|
+
out.append({
|
|
1417
|
+
"id": str(raw.get("id") or path.stem),
|
|
1418
|
+
"name": str(raw.get("name") or path.stem),
|
|
1419
|
+
"description": str(raw.get("description") or ""),
|
|
1420
|
+
"department": str(raw.get("department") or path.parent.parent.name),
|
|
1421
|
+
"tier": str(raw.get("tier") or ""),
|
|
1422
|
+
"command": str(raw.get("command") or ""),
|
|
1423
|
+
"phases_count": len(phases),
|
|
1424
|
+
"file": rel,
|
|
1425
|
+
"content": content,
|
|
1426
|
+
})
|
|
1427
|
+
return {"workflows": out}
|
|
1428
|
+
|
|
1429
|
+
|
|
1390
1430
|
# --- Sidebar stats widget (PR87d v3.22.0) ---
|
|
1391
1431
|
|
|
1392
1432
|
@app.get("/api/sidebar-stats")
|